在开发通常的业务来讲,不须要知道 Vue 中钩子函数过多的执行细节。可是若是你想写出足够稳健的代码,或者想开发一些通用库,那么就少不了要深刻了解各类钩子的执行时机了。javascript
先直接看一个例子:vue
import Vue from 'vue';
Vue.component('Test', {
props: {
name: String
},
template: `<div class="test">{{ name }}</div>`,
beforeCreate() {
console.log('Test beforeCreate');
},
created() {
console.log('Test created');
},
mounted() {
console.log('Test mounted');
},
beforeDestroy() {
console.log('Test beforeDestroy');
},
destroyed() {
console.log('Test destroyed');
},
beforeUpdate() {
console.log('Test beforeUpdate');
},
updated() {
console.log('Test updated');
}
});
Vue.component('Test1', {
props: {
name: String
},
template: '<div class="test1"><slot />{{ name }}</div>',
beforeCreate() {
console.log('Test1 beforeCreate');
},
created() {
console.log('Test1 created');
},
mounted() {
console.log('Test1 mounted');
},
beforeDestroy() {
console.log('Test1 beforeDestroy');
},
destroyed() {
console.log('Test1 destroyed');
},
beforeUpdate() {
console.log('Test1 beforeUpdate');
},
updated() {
console.log('Test1 updated');
}
});
new Vue({
el: '#app',
data() {
return {
a: true,
name: ''
};
},
mounted() {
setTimeout(() => {
console.log('-----------');
this.name = 'yibuyisheng1';
this.$nextTick(() => {
console.log('-----------');
});
}, 1000);
setTimeout(() => {
console.log('-----------');
this.a = false;
this.$nextTick(() => {
console.log('-----------');
});
}, 2000);
},
template: '<Test1 v-if="a" :name="name"><Test :name="name" /></Test1><span v-else></span>'
});
复制代码
运行这个例子,会发现输出以下:java
Test1 beforeCreate
Test1 created
Test beforeCreate
Test created
Test mounted
Test1 mounted
-----------
Test1 beforeUpdate
Test beforeUpdate
Test updated
Test1 updated
-----------
-----------
Test1 beforeDestroy
Test beforeDestroy
Test destroyed
Test1 destroyed
-----------
复制代码
很清楚地能够看到,各个钩子函数在组件树中调用的前后顺序。node
实际上,此处能够对照 DOM 事件的捕获和冒泡过程来看:算法
同时,能够看到,在初始化流程、 update 流程和销毁流程中,子级的相应声明周期方法都是在父级相应周期方法之间调用的。好比子级的初始化钩子函数( beforeCreate 、 created 、 mounted )都是在父级的 created 和 mounted 之间调用的,这实际上说明等到子级准备好了,父级才会将本身挂载到上一层 DOM 树中去,从而保证界面上不会闪现脏数据。bash
充分理解这个调用过程是颇有必要的,好比有下面两个很是常见的场景:app
在对话框组件的实现中,为了方便处理浮层遮盖问题,每每会将浮层根元素放置到 body 元素下面,而不是让其保持在书写对话框组件所在的位置。同时须要作一个浮层的层叠顺序管理,正确处理对话框相互之间的视觉覆盖关系。函数
为了达到这个效果,能够在对话框组件的 created 钩子函数中向全局层叠管理器注册本身,而后拿到本身的 z-index 值,而后在 mounted 的时候将浮层根元素插入到 body 元素下。ui
有不少这种类型的组件,好比 Select 和 Option 、 Tab 和 TabItem 、 Table 和 TableRow 等等。通常状况下,会采用子级组件向父级组件注册的方式来实现这种依赖关系,由于在子级的钩子函数中,能够明确地知道必定存在父级组件,因此往上查找起来会很是方便。this
在 Vue 中,能够定义指令:
Vue.directive('mydirective', {
bind() {},
inserted() {},
update() {},
componentUpdated() {},
unbind() {}
});
复制代码
指令中有五个钩子函数,要搞清楚这五个函数的具体执行时机,得结合 Vue 的 diff 过程来看。
在 diff 过程当中,会对同级相同类型的节点进行对比更新,实际上就是对老的虚拟 DOM 节点( oldVnode )和新的虚拟 DOM 节点( newVnode )进行对比更新。
若是是第一次渲染,那么 oldVnode 会被设置成一个空节点( emptyVnode ),方便复用对比更新逻辑。
这个新老虚拟节点的比对过程,天然也包括虚拟节点上的指令的比对。在对指令进行对比的时候,会确保虚拟节点对应的真实 DOM 节点已经建立出来了。
若是是建立流程,那么就是 oldEmptyVnode 和 newVnode 对比,其中 newVnode 上面已经关联好了相应的 DOM 节点,此时直接就调用 bind
钩子函数了。
而后在 DOM 节点插入父 DOM 节点以后,就调用 inserted
钩子函数。
bind
只会在指令和 DOM 节点绑定的时候才会被调用。
inserted
只会在 DOM 节点插入到父 DOM 节点时才会被调用。
若是某个组件数据发生了变化,须要调用 render 方法从新渲染,那么这就会引发一个在组件范围内的更新流程,该组件下的虚拟节点树(直观感觉就是组件模板里面写的那些节点)就会进行新老比对,走 diff 流程。
若是碰到带指令的 VNode ,就要进行指令 diff 了,在这个过程当中就会调用 updated
钩子函数。
而后执行后续 VNode 比对,等都 diff 完了以后,就会当即调用以前带指令 VNode 的 componentUpdated
钩子函数了。
在指令与 DOM 节点解除绑定的时候,会调用 unbind
钩子函数。
流程理论描述老是苍白的,有时候很难让人快速理解,因此此处用一些简单的例子进行说明。
import Vue from 'vue';
Vue.directive('dir', {
bind(el) {
console.log('dir bind');
console.log(!!el.parentNode);
},
inserted(el) {
console.log('dir inserted');
console.log(!!el.parentNode);
},
update(el) {
console.log('dir update');
console.log('-----', el.textContent);
},
componentUpdated(el) {
console.log('dir componentUpdated');
console.log('-----', el.textContent);
},
unbind(el) {
console.log('dir unbind');
console.log(!!el.parentNode);
}
});
Vue.component('Test', {
props: {
name: String,
shouldBind: Boolean
},
template: `<div><b>{{ name }}</b><span v-if="shouldBind" v-dir>{{ name }}</span></div>`
});
new Vue({
el: '#app',
data() {
return {
name: '',
shouldBind: true
};
},
mounted() {
setTimeout(() => {
this.name = 'yibuyisheng';
}, 1000);
setTimeout(() => {
this.shouldBind = false;
}, 2000);
},
template: '<Test :name="name" :should-bind="shouldBind" />'
});
复制代码
在上述例子中,构造了一个自定义指令 dir
,而后在每一个钩子函数里面都打印各自的一些内容。
在 Test 组件中,有一个 span 元素使用了 dir
指令,而且该元素受 shouldBind 变量控制,若是该变量为假值,那么指令和 DOM 元素就会解除绑定。组件模板中访问了 name ,方便经过改变 name 引发组件从新 render 。
执行上述代码,能够看到以下输出:
dir bind
false
dir inserted
true
dir update
-----
dir componentUpdated
----- yibuyisheng
dir unbind
false
复制代码
在初始化 diff 的时候, name 为空字符串, shouldBind 为 true ,那么渲染出来的 DOM 树为:
<div><b></b><span></span></div>
复制代码
在这个过程当中, dir
指令要与 span 元素绑定,因此会调用 bind
钩子函数,输出 dir bind
。同时在 bind
的时候, span 元素尚未被插入父元素( div )中,所以输出了 false
。
在 span 元素插入父元素( div )以后,会立刻调用 inserted
钩子函数,输出 dir inserted
和 true
。
过了一秒以后, name 值变为 yibuyisheng
,触发了 Test 组件调用 render ,触发 diff 流程。在作 span 元素对应的新老虚拟节点对比的时候,就会调用 dir
指令的 update
钩子函数,输出 dir update
,可是此时 name 数据尚未更新到 DOM 树中去,所以拿到的 span 的 textContent 仍是 -----
,输出 -----
。
同步 diff 走完子孙虚拟节点以后, name 的值已经被更新到 DOM 树中去了,此时会调用 componentUpdated
钩子函数,输出 dir componentUpdated
和 ----- yibuyisheng
。
再过一秒以后, shouldBind 变为 false ,触发 Test 组件的 render ,继而走 diff 流程。在 span 元素的指令 diff 过程当中,发现 span 元素应当被移除,所以会解绑 span 元素和指令,因此会调用 dir 的 unbind
钩子函数,输出 dir unbind
,同时由于 span 元素已经被移除了,因此也不存在父元素了,最终输出 false
。
指令钩子函数的这种机制,结合 diff 算法中的 DOM 节点复用,会有一点意想不到的结果:
<template>
<section>
<div v-if="someCondition" a="1"></div>
<div v-else v-some-directive></div>
</section>
</template>
<script> export default { directives: { 'some-condition': { bind() { console.log('bind'); }, inserted() { console.log('inserted'); }, unbind() { console.log('unbind'); } } }, data() { return { someCondition: true }; }, mounted() { this.$el.firstElementChild.__id = 1; setTimeout(() => { this.someCondition = false; console.log(this.$el.firstElementChild.__id); }, 1000); setTimeout(() => { this.someCondition = true; console.log(this.$el.firstElementChild.__id); }, 2000); setTimeout(() => { this.someCondition = false; console.log(this.$el.firstElementChild.__id); }, 3000); } }; </script>
复制代码
上述代码的输出为:
1
bind
inserted
1
unbind
1
bind
inserted
复制代码
从输出结果中发现, this.$el.firstElementChild.__id
的值所有是 1 ,说明整个过程只有一个 div 元素, div 元素被复用了。
示例中,对第一个 div 元素加了一个 a="1"
属性,主要是为了保证两个 div 虚拟节点能被断定为同类型的虚拟节点。
在初始化的时候, someCondition 为 true ,对应模板中的 v-if 分支生效。
一秒后, someCondition 为 false ,对应模板中的 v-else 分支生效,此时由于两个 div 虚拟节点是同类型的,所以会复用以前生成的 div DOM 元素,同时将 v-some-directive 指令与该元素关联起来,所以输出了第一组 bind
、 inserted
。
再过一秒后, someCondition 为 true ,对应模板中 v-if 分支生效, v-else 分支生效,一样复用以前的 div DOM 元素,同时将 v-some-directive 与 div DOM 元素解绑,调用指令的 unbind 钩子函数,输出 unbind
。
再过一秒, someCondition 变为 true ,重复前述过程。
这里要注意,在官方文档中,关于 inserted 钩子函数的描述是这样的:
inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不必定已被插入文档中)。
从上面这个例子能够看出,这句描述是很是不严谨的,由于在第三秒的时候,并无发生被绑定元素被插入父节点的过程,可是却调用了 inserted 钩子函数。