vue中的钩子函数

在开发一般的业务来说,不需要知道 Vue 中钩子函数过多的执行细节。但是如果你想写出足够稳健的代码,或者想开发一些通用库,那么就少不了要深入了解各种钩子的执行时机了。

组件生命周期 hook 在组件树中的调用时机

先直接看一个例子:

 

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 等等。一般情况下,会采用子级组件向父级组件注册的方式来实现这种依赖关系,因为在子级的钩子函数中,可以明确地知道一定存在父级组件,所以往上查找起来会非常方便。

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

在 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 钩子函数。



作者:yibuyisheng
链接:https://www.jianshu.com/p/3e91a1c42397
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值