vue-重新研究生命周期

Vue 的生命周期:beforeCreate,created,beforeMount,beforeUpdate, mounted,beforeUpdate,updated,updated, beforeDestroy,destroy

主要参考来源:

Vue 的生命周期是个啥?

每个 Vue 组件都是一个个的 Vue 实例,当一个 Vue 实例被创建时,都会有一系列的初始化过程,每个过程都会执行一些特定的方法。
学过 JAVA 或者 PHP 的都应该知道,对象在被实例化和被销毁的时候也有一个过程,期间会调用构造函数,析构函数;这个和 Vue 生命周期类似。

官网的示意图

vue-重新研究生命周期_01

复习每个生命周期钩子

  • beforeCreate(创建前)
    在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用。
    此时组件上的对象还未被创建,el 和 data 都没有初始化,也不能调用 methods,data,computed,等方法和数据

  • create(创建后)
    在实例创建完成后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。然而,挂载阶段还没开始,且 $el property 目前尚不可用。
    总之就是,除了页面还没有开始渲染,其他的工作已经准备就绪,包括数据观测,树形和方法调用,watch/event事件回调,data初始化。可以进行 ajax 请求数据了,但是最好不要,毕竟此时的页面还是空白

  • beforeMount
    在挂载开始之前被调用:相关的 render 函数首次被调用。
    实例已完成以下的配置: 编译模板,把data里面的数据和模板生成html,完成了el和data 初始化,但是此时还没有挂在html到页面上,还是没有 dom。

  • mounted
    挂载完成了,html已经渲染到了document。可以进行ajax请求了。
    官方温馨提示: mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用 vm.$nextTick

  • beforeUpdate
    数据更新之后,dom 被更新之前调用。注意官方的一句话:这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器

  • updated
    在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。这个时候dom已经更新完毕,可以进行依赖于dom的操作,但是,你应该避免在此期间更改状态。如果要相应状态改变,因为这可能会导致更新无限循环

  • beforeDestroy
    实例销毁之前调用。在这一步,实例仍然完全可用。

    1. 这一步还可以用this来获取实例
    2. 一般在这一步做一些重置的操作,比如清除掉组件中的定时器 和 监听的dom事件
  • destroyed
    实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁

以上复习了生命周期钩子函数在各个阶段都干了什么。然后用实例来验证加强记忆一下,顺便探讨父子组件的生命周期表现是什么样的。

单组件的生命周期

简单两个组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<template>
<div class="main">
<span id="tip_text">我是一个测试生命周期的组件</span>
</div>
</template>

<script>
export default {
name: 'LifeCycle',
data () {
return {
name: '我是谁'
}
},
beforeCreate() {
console.log('**** parent beforeCreate');
console.log('**** parent beforeCreate -this.name:', this.name);
console.log('**** parent beforeCreate -test dom:', document.getElementById('tip_text'));
},
created() {
console.log('**** parent created');
console.log('**** parent created -this.name:', this.name);
console.log('**** parent created -test dom:', document.getElementById('tip_text'));
},
beforeMount() {
console.log('**** parent beforeMount');
console.log('**** parent beforeMount -this.name:', this.name);
console.log('**** parent beforeMount -test dom:', document.getElementById('tip_text'));
},
mounted() {
console.log('**** parent mounted');
console.log('**** parent mounted -this.name:', this.name);
console.log('**** parent mounted -test dom:', document.getElementById('tip_text'));
},
beforeUpdate() {
console.log('**** parent beforeUpdate');
console.log('**** parent beforeUpdate -this.name:', this.name);
console.log('**** parent beforeUpdate -test dom:', document.getElementById('tip_text'));
},
updated() {
console.log('**** parent updated');
console.log('**** parent updated -this.name:', this.name);
console.log('**** parent updated -test dom:', document.getElementById('tip_text'));
},
beforeDestroy() {
console.log('**** parent beforeDestroy');
console.log('**** parent beforeDestroy -this.name:', this.name);
console.log('**** parent beforeDestroy -test dom:', document.getElementById('tip_text'));
},
destroyed() {
console.log('**** parent destroyed');
console.log('**** parent destroyed -this.name:', this.name);
console.log('**** parent destroyed -test dom:', document.getElementById('tip_text'));
},
methods: {}
}
</script>

模拟页面进入,控制台输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
**** parent beforeCreate
**** parent beforeCreate -this.name: undefined
**** parent beforeCreate -test dom: null
**** parent created
**** parent created -this.name: 我是谁
**** parent created -test dom: null
**** parent beforeMount
**** parent beforeMount -this.name: 我是谁
**** parent beforeMount -test dom: null
**** parent mounted
**** parent mounted -this.name: 我是谁
**** parent mounted -test dom: <span id=​"tip_text">​我是一个测试生命周期的组件​</span>​

从上面可以看到,组件在创建的时候

  • 依次执行:beforeCreate > created > beforeMount > mounted;
  • beforeCreate 期间,不能访问data,而 created之后就可以访问 data 了;
  • mounted 时期,不仅能访问data,dom元素也可以正确获取;

切换路由,模拟页面离开,控制台输出如下:

1
2
3
4
5
6
**** parent beforeDestroy
**** parent beforeDestroy -this.name: 我是谁
**** parent beforeDestroy -test dom: null
**** parent destroyed
**** parent destroyed -this.name: 我是谁
**** parent destroyed -test dom: null

组件离开被销毁时候,验证出以下观点

  • 依次执行:beforeDestroy > destroyed;
  • beforeDestroy 期间,还能访问 data,但是 dom 此时已经被销毁了;
  • destroyed 时期,竟然还能访问data?有点疑惑

发现updated和beforeUpdate并没有参与组件的创建和销毁,尝试加入动作;

调整组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div class="main">
<span>我是一个测试生命周期的组件</span>
<div>
<span id="tip_text">点击测试 {{ num }} </span>
<button @click="onClickCount">点击</button>
</div>
</div>
</template>
<script>
{
data () {
return {
name: '我是谁',
num: 1,
}
},
methods: {
onClickCount() {
this.num++;
}
}
}
</script>

1
2
3
4
5
6
**** parent beforeUpdate
**** parent beforeUpdate -this.name: 我是谁
**** parent beforeUpdate -test dom: <span id="tip_text">点击测试 3 </span>
**** parent updated
**** parent updated -this.name: 我是谁
**** parent updated -test dom: <span id="tip_text">点击测试 3 </span>

组件嵌套的生命周期会如何表现呢?

先来两个组件

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<template>
<div class="main">
<span>我是一个测试生命周期的组件</span>
<div>
<span>输入的内容:{{ title || '-' }}</span>
</div>

<SlInput @slInput="onSlInput" :title="title"/>
</div>
</template>

<script>
import SlInput from './SlInput.vue'
export default {
name: 'LifeCycle',
components: { SlInput },
data () {
return {
name: '我是谁',
title: '',
num: 1,
}
},
beforeCreate() {
console.log('**** parent beforeCreate');
},
created() {
console.log('**** parent created');
},
beforeMount() {
console.log('**** parent beforeMount');
},
mounted() {
console.log('**** parent mounted');
},
beforeUpdate() {
console.log('**** parent beforeUpdate');
},
updated() {
console.log('**** parent updated');
},
beforeDestroy() {
console.log('**** parent beforeDestroy');
},
destroyed() {
console.log('**** parent destroyed');
},
methods: {

onSlInput(value) {
this.title = value
}
}
}
</script>

子组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<template>
<input type="text" :value="title" @input="onInput">
</template>

<script>
export default {
name: 'SlInput',
data() {
return {}
},
props: ['title'],
beforeCreate() {
console.log('**** children beforeCreate');
},
created() {
console.log('**** children created');
},
beforeMount() {
console.log('**** children beforeMount');
},
mounted() {
console.log('**** children mounted');
},
beforeUpdate() {
console.log('**** children beforeUpdate');
},
updated() {
console.log('**** children updated');
},
beforeDestroy() {
console.log('**** children beforeDestroy');
},
destroyed() {
console.log('**** children destroyed');
},
methods: {
onInput(event) {
this.$emit('slInput', event.target.value)
}
}
}
</script>

同样再次模拟页面进入,控制台输出如下:

1
2
3
4
5
6
7
8
**** parent beforeCreate
**** parent created
**** parent beforeMount
**** children beforeCreate
**** children created
**** children beforeMount
**** children mounted
**** parent mounted

可以发现,父子组件嵌套情况下,组件实例化时:

  • 父组件先进行实例化,在模板挂载之前,进行子组件实例化,父组件会等待子组件实例化完毕,再进行父组件自己的模板挂载
    beforeCreate >
    created >
    beforeMount >
    children-beforeCreate >
    children-created >
    children-beforeMount >
    children-mounted >
    mounted

模拟页面离开,控制台输出如下:

1
2
3
4
**** parent beforeDestroy
**** children beforeDestroy
**** children destroyed
**** parent destroyed

父子组件嵌套,父组件在销毁时:

  • 父组件先准备进行销毁,进入 调用钩子 beforeDestroy,等待子组件销毁完毕,父组件继续执行销毁,调用钩子 destroyed,执行顺序如下:
    parent beforeDestroy >
    children-beforeDestroy >
    children-destroyed >
    destroyed

何时调用 update 呢?

尝试子组件和父组件通信,修改父组件的 data

1
2
3
4
**** parent beforeUpdate
**** children beforeUpdate
**** children updated
**** parent updated

尝试父组件修改子组件data

父组件组件添加以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  <div>
<button @click="onClickSetChildrenCount">点击修改子组件data</button>
</div>

<script>
{

methods: {
onClickSetChildrenCount() {
this.$refs.SlInput.childrenCount++
}
}
}
</script>

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
 <div>
子组件data修改测试:{{ childrenCount }}
</div>

<script>
{
data() {
return {
childrenCount: 1
}
}
}
</script>

点击按钮触发,修改子组件的data,控制台输出如下

1
2
**** children beforeUpdate
**** children updated

此时没有父组件的参与

单修改父组件data,会触发子组件的 update 吗?

还是那个点击测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div>
<span id="tip_text">点击测试 {{ num }} </span>
<button @click="onClickCount">点击</button>
</div>
<script>
{
data() {
return {
childrenCount: 1
}
}
methods: {
onClickCount() {
this.num++;
},
}
}
</script>

点击触发事件,控制台输出

1
2
**** parent beforeUpdate
**** parent updated

此时没有子组件的参与

子组件创建和销毁,父组件生命周期是个什么表现?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div class="main">
<h1>我是一个测试生命周期的组件</h1>

<hr />
子组件在这:
<SlInput id="tip_text" :title="title" v-if="isShow" />
<hr />
<div>
<button @click="onClickToggle">点击{{ isShow ? '隐藏' : '显示' }}</button>
</div>
</div>
</template>
<script>
{
data() {
return {
isShow: true
}
}
methods: {
onClickToggle() {
this.isShow = !this.isShow
},
}
}
</script>

点击隐藏子组件

1
2
3
4
5
6
**** parent beforeUpdate
**** parent beforeUpdate -test dom: <div id=​"tip_text">​…​</div>​
**** children beforeDestroy
**** children destroyed
**** parent updated
**** parent updated -test dom: null

模拟子组件销毁,可以看出:

  • 子组件销毁之前,父组件进入 beforeUpdate;
  • 等子组件销毁完毕,继续进行父组件的 updated,父组件更新完毕;

点击显示子组件

1
2
3
4
5
6
7
8
**** parent beforeUpdate
**** parent beforeUpdate -test dom: null
**** children beforeCreate
**** children created
**** children beforeMount
**** children mounted
**** parent updated
**** parent updated -test dom: <div id=​"tip_text">​…​</div>​

父组件内,模拟初始化子组件,可以看出:

  • 子组件实例化之前,父组件进入 beforeUpdate;
  • 等子组件挂载完毕,继续进行父组件的 updated,父组件更新完毕;

组件嵌套时,生命周期总结

  • 子组件的创建销毁会触发父组件的 update;
  • 子组件的 update,不一定会触发父组件的 update;
  • 父组件的 update,不一定会触发子组件的 update;