理清 Vue 中的钩子函数

在开发通常的业务来讲,不须要知道 Vue 中钩子函数过多的执行细节。可是若是你想写出足够稳健的代码,或者想开发一些通用库,那么就少不了要深刻了解各类钩子的执行时机了。javascript

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

先直接看一个例子: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 事件的捕获和冒泡过程来看:算法

  • beforeCreate 、 created 、 beforeUpdate 、 beforeDestroy 是在“捕获”过程当中调用的;
  • mounted 、 updated 、 destroyed 是在“冒泡”过程当中调用的。

同时,能够看到,在初始化流程、 update 流程和销毁流程中,子级的相应声明周期方法都是在父级相应周期方法之间调用的。好比子级的初始化钩子函数( beforeCreate 、 created 、 mounted )都是在父级的 created 和 mounted 之间调用的,这实际上说明等到子级准备好了,父级才会将本身挂载到上一层 DOM 树中去,从而保证界面上不会闪现脏数据。bash

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

实现对话框组件

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

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

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

有不少这种类型的组件,好比 Select 和 Option 、 Tab 和 TabItem 、 Table 和 TableRow 等等。通常状况下,会采用子级组件向父级组件注册的方式来实现这种依赖关系,由于在子级的钩子函数中,能够明确地知道必定存在父级组件,因此往上查找起来会很是方便。this

指令生命周期 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 insertedtrue

过了一秒以后, 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 指令与该元素关联起来,所以输出了第一组 bindinserted

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

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

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

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

从上面这个例子能够看出,这句描述是很是不严谨的,由于在第三秒的时候,并无发生被绑定元素被插入父节点的过程,可是却调用了 inserted 钩子函数。

相关文章
相关标签/搜索