一.描述

bind:

只调用一次,指令第一次绑定到元素时调用。用这个钩子函数可以定义一个在绑定时执行一次的初始化动作。4

inserted

被绑定元素插入父节点时调用(父节点存在即可调用)。

update

所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新

componentUpdated

指令所在组件的 VNode 及其子 VNode 全部更新后调用。

unbind:

只调用一次, 指令与元素解绑时调用。

二.自定义指令钩子函数参数

el:指令所绑定的元素,可以用来直接操作 DOM 。
binding:一个对象,包含以下属性
name:指令名,不包含v-的前缀;
value:指令的绑定值;例如:v-my-directive=”1+1”,value的值是2;
oldValue:指令绑定的前一个值,仅在update和componentUpdated钩子函数中可用,无论值是否改变都可用;
expression:绑定值的字符串形式;例如:v-my-directive=”1+1”,expression的值是’1+1’;
arg:传给指令的参数;例如:v-my-directive:foo,arg的值为 ‘foo’;
modifiers:一个包含修饰符的对象;例如:v-my-directive.a.b,modifiers的值为{‘a’:true,’b’:true}
vnode:Vue编译的生成虚拟节点;
oldVnode:上一次的虚拟节点,仅在update和componentUpdated钩子函数中可用。

三.常见场景

充分理解这个调用过程是很有必要的,比如有下面两个非常常见的场景:

(一)实现对话框组件

在对话框组件的实现中,为了方便处理浮层遮盖问题,往往会将浮层根元素放置到 body 元素下面,而不是让其保持在书写对话框组件所在的位置。同时需要做一个浮层的层叠顺序管理,正确处理对话框相互之间的视觉覆盖关系。

为了达到这个效果,可以在对话框组件的created钩子函数中向全局层叠管理器注册自己,然后拿到自己的 z-index 值,然后在 mounted 的时候将浮层根元素插入到 body 元素下。

实现有依赖关系的父子组件

有很多这种类型的组件,比如 SelectOptionTabTabItemTableTableRow 等等。一般情况下,会采用子级组件向父级组件注册的方式来实现这种依赖关系,因为在子级的钩子函数中,可以明确地知道一定存在父级组件,所以往上查找起来会非常方便。

指令生命周期 hook 的调用时机

在 Vue 中,可以定义指令:

  1. Vue.directive('mydirective', {
  2. bind() {},
  3. inserted() {},
  4. update() {},
  5. componentUpdated() {},
  6. unbind() {}
  7. });

指令中有五个钩子函数,要搞清楚这五个函数的具体执行时机,得结合 Vue 的 diff 过程来看。

在 diff 过程中,会对同级相同类型的节点进行对比更新,实际上就是对老的虚拟 DOM 节点( oldVnode )和新的虚拟 DOM 节点(newVnode)进行对比更新。

如果是第一次渲染,那么 oldVnode会被设置成一个空节点( emptyVnode ),方便复用对比更新逻辑。

这个新老虚拟节点的比对过程,自然也包括虚拟节点上的指令的比对。在对指令进行对比的时候,会确保虚拟节点对应的真实 DOM 节点已经创建出来了。

创建流程

如果是创建流程,那么就是 oldEmptyVnodenewVnode对比,其中 newVnode 上面已经关联好了相应的 DOM 节点,此时直接就调用 bind 钩子函数了。

然后在 DOM 节点插入父 DOM 节点之后,就调用 inserted钩子函数。

bind只会在指令和 DOM 节点绑定的时候才会被调用。

inserted 只会在 DOM 节点插入到父 DOM 节点时才会被调用。

更新流程

如果某个组件数据发生了变化,需要调用 render 方法重新渲染,那么这就会引起一个在组件范围内的更新流程,该组件下的虚拟节点树(直观感受就是组件模板里面写的那些节点)就会进行新老比对,走 diff 流程。

如果碰到带指令的 VNode ,就要进行指令 diff 了,在这个过程中就会调用 updated 钩子函数。

然后执行后续 VNode 比对,等都 diff 完了之后,就会立即调用之前带指令 VNode 的 componentUpdated 钩子函数了。

解绑销毁

在指令与 DOM 节点解除绑定的时候,会调用 unbind 钩子函数。

实例

流程理论描述总是苍白的,有时候很难让人快速理解,所以此处用一些简单的例子进行说明。

基本例子

  1. import Vue from 'vue';
  2. Vue.directive('dir', {
  3. bind(el) {
  4. console.log('dir bind');
  5. console.log(!!el.parentNode);
  6. },
  7. inserted(el) {
  8. console.log('dir inserted');
  9. console.log(!!el.parentNode);
  10. },
  11. update(el) {
  12. console.log('dir update');
  13. console.log('-----', el.textContent);
  14. },
  15. componentUpdated(el) {
  16. console.log('dir componentUpdated');
  17. console.log('-----', el.textContent);
  18. },
  19. unbind(el) {
  20. console.log('dir unbind');
  21. console.log(!!el.parentNode);
  22. }
  23. });
  24. Vue.component('Test', {
  25. props: {
  26. name: String,
  27. shouldBind: Boolean
  28. },
  29. template: `<div><b>{{ name }}</b><span v-if="shouldBind" v-dir>{{ name }}</span></div>`
  30. });
  31. new Vue({
  32. el: '#app',
  33. data() {
  34. return {
  35. name: '',
  36. shouldBind: true
  37. };
  38. },
  39. mounted() {
  40. setTimeout(() => {
  41. this.name = 'yibuyisheng';
  42. }, 1000);
  43. setTimeout(() => {
  44. this.shouldBind = false;
  45. }, 2000);
  46. },
  47. template: '<Test :name="name" :should-bind="shouldBind" />'
  48. });

在上述例子中,构造了一个自定义指令 dir ,然后在每个钩子函数里面都打印各自的一些内容。

在 Test 组件中,有一个 span 元素使用了 dir 指令,并且该元素受 shouldBind 变量控制,如果该变量为假值,那么指令和 DOM 元素就会解除绑定。组件模板中访问了 name ,方便通过改变 name 引起组件重新 render 。

执行上述代码,可以看到如下输出:

  1. dir bind
  2. false
  3. dir inserted
  4. true
  5. dir update
  6. -----
  7. dir componentUpdated
  8. ----- yibuyisheng
  9. dir unbind
  10. false

在初始化 diff 的时候,name 为空字符串, shouldBindtrue ,那么渲染出来的 DOM 树为:

  1. <div><b></b><span></span></div>

在这个过程中, dir 指令要与 span 元素绑定,所以会调用 bind 钩子函数,输出 dir bind 。同时在 bind 的时候, span 元素还没有被插入父元素( div )中,因此输出了 false 。

在 span 元素插入父元素( div )之后,会马上调用 inserted 钩子函数,输出 dir insertedtrue

过了一秒之后, name值变为 yibuyisheng ,触发了 Test 组件调用 render ,触发 diff 流程。在做 span 元素对应的新老虚拟节点对比的时候,就会调用 dir 指令的 update 钩子函数,输出 dir update ,但是此时 name 数据还没有更新到 DOM 树中去,因此拿到的spantextContent 还是 ——- ,输出 ——- 。

同步 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 节点复用,会有一点意想不到的结果:

  1. <template>
  2. <section>
  3. <div v-if="someCondition" a="1"></div>
  4. <div v-else v-some-directive></div>
  5. </section>
  6. </template>
  7. <script>
  8. export default {
  9. directives: {
  10. 'some-condition': {
  11. bind() {
  12. console.log('bind');
  13. },
  14. inserted() {
  15. console.log('inserted');
  16. },
  17. unbind() {
  18. console.log('unbind');
  19. }
  20. }
  21. },
  22. data() {
  23. return {
  24. someCondition: true
  25. };
  26. },
  27. mounted() {
  28. this.$el.firstElementChild.__id = 1;
  29. setTimeout(() => {
  30. this.someCondition = false;
  31. console.log(this.$el.firstElementChild.__id);
  32. }, 1000);
  33. setTimeout(() => {
  34. this.someCondition = true;
  35. console.log(this.$el.firstElementChild.__id);
  36. }, 2000);
  37. setTimeout(() => {
  38. this.someCondition = false;
  39. console.log(this.$el.firstElementChild.__id);
  40. }, 3000);
  41. }
  42. };
  43. </script>

上述代码的输出为:

  1. 1
  2. bind
  3. inserted
  4. 1
  5. unbind
  6. 1
  7. bind
  8. inserted

从输出结果中发现, this.$el.firstElementChild.__id 的值全部是 1 ,说明整个过程只有一个 div 元素, div 元素被复用了。

示例中,对第一个 div 元素加了一个 a="1"属性,主要是为了保证两个 div 虚拟节点能被判定为同类型的虚拟节点。

在初始化的时候, someConditiontrue ,对应模板中的 v-if 分支生效。

一秒后, someConditionfalse ,对应模板中的 v-else 分支生效,此时因为两个 div 虚拟节点是同类型的,因此会复用之前生成的 div DOM 元素,同时将 v-some-directive 指令与该元素关联起来,因此输出了第一组 bindinserted

再过一秒后, someConditiontrue ,对应模板中 v-if 分支生效, v-else 分支生效,同样复用之前的 div DOM 元素,同时将 v-some-directivediv DOM 元素解绑,调用指令的 unbind 钩子函数,输出 unbind

再过一秒, someCondition 变为 true ,重复前述过程。

这里要注意,在官方文档中,关于 inserted 钩子函数的描述是这样的:

inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

从上面这个例子可以看出,这句描述是非常不严谨的,因为在第三秒的时候,并没有发生被绑定元素被插入父节点的过程,但是却调用了 inserted 钩子函数。

四.案例

  1. <div id="app">
  2. <my-comp v-if="msg" :msg="msg"></my-comp>
  3. <button @click="update">更新</button>
  4. <button @click="uninstall">卸载</button>
  5. <button @click="install">安装</button>
  6. </div>
  7. <script type="text/javascript">
  8. Vue.directive('hello', {
  9. bind: function (el){
  10. console.log('bind');
  11. },
  12. inserted: function (el){
  13. console.log('inserted');
  14. },
  15. update: function (el){
  16. console.log('update');
  17. },
  18. componentUpdated: function (el){
  19. console.log('componentUpdated');
  20. },
  21. unbind: function (el){
  22. console.log('unbind');
  23. }
  24. });
  25. var myComp = {
  26. template: '<h1 v-hello>{{msg}}</h1>',
  27. props: {
  28. msg: String
  29. }
  30. }
  31. new Vue({
  32. el: '#app',
  33. data: {
  34. msg: 'Hello'
  35. },
  36. components: {
  37. myComp: myComp
  38. },
  39. methods: {
  40. update: function (){
  41. this.msg = 'Hi';
  42. },
  43. uninstall: function (){
  44. this.msg = '';
  45. },
  46. install: function (){
  47. this.msg = 'Hello';
  48. }
  49. }
  50. })
  51. </script>

注意区别

bind与inserted:bind时父节点为null,inserted时父节点存在;
update与componentUpdated:update是数据更新前,componentUpdated是数据更新后。

函数简写

之前的写法:

  1. Vue.directive("color",{
  2. bind(el,binding){ //只会执行一次!
  3. el.style.backgroundColor = binding.value
  4. },
  5. update(el,binding){
  6. el.style.backgroundColor = binding.value
  7. }
  8. })

简写

大多数情况下,我们可能想在 bind 和 update 钩子上做重复动作,并且不想关心其它的钩子函数。可以这样写:

  1. Vue.directive('color-swatch', function (el, binding) {
  2. el.style.backgroundColor = binding.value
  3. })