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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342