Vue实例从创建到销毁的过程,就是生命周期。详细来说也就是从开始创建、初始化数据、编译模板、挂载Dom、渲染→更新→渲染、卸载等一系列过程。
首先我们来看一下生命周期图:
钩子在组件树中的调用时机
先直接看一个例子:
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>' });
运行这个例子,会发现输出如下:
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 事件的捕获和冒泡过程来看:
- beforeCreate 、 created 、 beforeUpdate 、 beforeDestroy 是在“捕获”过程中调用的;
- mounted 、 updated 、 destroyed 是在“冒泡”过程中调用的。
同时,可以看到,在初始化流程、 update 流程和销毁流程中,子级的相应声明周期方法都是在父级相应周期方法之间调用的。比如子级的初始化钩子函数( beforeCreate 、 created 、 mounted )都是在父级的 created 和 mounted 之间调用的,这实际上说明等到子级准备好了,父级才会将自己挂载到上一层 DOM 树中去,从而保证界面上不会闪现脏数据。
充分理解这个调用过程是很有必要的,比如有下面两个非常常见的场景:
实现对话框组件
在对话框组件的实现中,为了方便处理浮层遮盖问题,往往会将浮层根元素放置到 body 元素下面,而不是让其保持在书写对话框组件所在的位置。同时需要做一个浮层的层叠顺序管理,正确处理对话框相互之间的视觉覆盖关系。
为了达到这个效果,可以在对话框组件的 created 钩子函数中向全局层叠管理器注册自己,然后拿到自己的 z-index 值,然后在 mounted 的时候将浮层根元素插入到 body 元素下。
实现有依赖关系的父子组件
有很多这种类型的组件,比如 Select 和 Option 、 Tab 和 TabItem 、 Table 和 TableRow 等等。一般情况下,会采用子级组件向父级组件注册的方式来实现这种依赖关系,因为在子级的钩子函数中,可以明确地知道一定存在父级组件,所以往上查找起来会非常方便。
指令生命周期钩子的调用时机
在 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
。
DOM 节点复用
指令钩子函数的这种机制,结合 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 钩子函数。
来源:https://www.cnblogs.com/johnvwan/p/12163385.html