在开发通常的业务来讲,不须要知道 Vue 中钩子函数过多的执行细节。可是若是你想写出足够稳健的代码,或者想开发一些通用库,那么就少不了要深刻了解各类钩子的执行时机了。html
先直接看一个例子: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>' });
运行这个例子,会发现输出以下:node
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 -----------
很清楚地能够看到,各个钩子函数在组件树中调用的前后顺序。算法
实际上,此处能够对照 DOM 事件的捕获和冒泡过程来看:app
同时,能够看到,在初始化流程、 update 流程和销毁流程中,子级的相应声明周期方法都是在父级相应周期方法之间调用的。好比子级的初始化钩子函数( beforeCreate 、 created 、 mounted )都是在父级的 created 和 mounted 之间调用的,这实际上说明等到子级准备好了,父级才会将本身挂载到上一层 DOM 树中去,从而保证界面上不会闪现脏数据。函数
充分理解这个调用过程是颇有必要的,好比有下面两个很是常见的场景:this
在对话框组件的实现中,为了方便处理浮层遮盖问题,每每会将浮层根元素放置到 body 元素下面,而不是让其保持在书写对话框组件所在的位置。同时须要作一个浮层的层叠顺序管理,正确处理对话框相互之间的视觉覆盖关系。spa
为了达到这个效果,能够在对话框组件的 created 钩子函数中向全局层叠管理器注册本身,而后拿到本身的 z-index 值,而后在 mounted 的时候将浮层根元素插入到 body 元素下。code
有不少这种类型的组件,好比 Select 和 Option 、 Tab 和 TabItem 、 Table 和 TableRow 等等。通常状况下,会采用子级组件向父级组件注册的方式来实现这种依赖关系,由于在子级的钩子函数中,能够明确地知道必定存在父级组件,因此往上查找起来会很是方便。component
在 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 钩子函数。