理清 Vue 中的鉤子函數(shù)

在開發(fā)一般的業(yè)務來說,不需要知道 Vue 中鉤子函數(shù)過多的執(zhí)行細節(jié)。但是如果你想寫出足夠穩(wěn)健的代碼,或者想開發(fā)一些通用庫,那么就少不了要深入了解各種鉤子的執(zhí)行時機了。

組件生命周期 hook 在組件樹中的調(diào)用時機

先直接看一個例子:

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>'
});

運行這個例子,會發(fā)現(xiàn)輸出如下:

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
-----------

很清楚地可以看到,各個鉤子函數(shù)在組件樹中調(diào)用的先后順序。

實際上,此處可以對照 DOM 事件的捕獲和冒泡過程來看:

  • beforeCreate 、 created 、 beforeUpdate 、 beforeDestroy 是在“捕獲”過程中調(diào)用的;
  • mounted 、 updated 、 destroyed 是在“冒泡”過程中調(diào)用的。

同時,可以看到,在初始化流程、 update 流程和銷毀流程中,子級的相應聲明周期方法都是在父級相應周期方法之間調(diào)用的。比如子級的初始化鉤子函數(shù)( beforeCreate 、 created 、 mounted )都是在父級的 created 和 mounted 之間調(diào)用的,這實際上說明等到子級準備好了,父級才會將自己掛載到上一層 DOM 樹中去,從而保證界面上不會閃現(xiàn)臟數(shù)據(jù)。

充分理解這個調(diào)用過程是很有必要的,比如有下面兩個非常常見的場景:

實現(xiàn)對話框組件

在對話框組件的實現(xiàn)中,為了方便處理浮層遮蓋問題,往往會將浮層根元素放置到 body 元素下面,而不是讓其保持在書寫對話框組件所在的位置。同時需要做一個浮層的層疊順序管理,正確處理對話框相互之間的視覺覆蓋關系。

為了達到這個效果,可以在對話框組件的 created 鉤子函數(shù)中向全局層疊管理器注冊自己,然后拿到自己的 z-index 值,然后在 mounted 的時候?qū)⒏痈夭迦氲?body 元素下。

實現(xiàn)有依賴關系的父子組件

有很多這種類型的組件,比如 Select 和 Option 、 Tab 和 TabItem 、 Table 和 TableRow 等等。一般情況下,會采用子級組件向父級組件注冊的方式來實現(xiàn)這種依賴關系,因為在子級的鉤子函數(shù)中,可以明確地知道一定存在父級組件,所以往上查找起來會非常方便。

指令生命周期 hook 的調(diào)用時機

在 Vue 中,可以定義指令:

Vue.directive('mydirective', {
    bind() {},
    inserted() {},
    update() {},
    componentUpdated() {},
    unbind() {}
});

指令中有五個鉤子函數(shù),要搞清楚這五個函數(shù)的具體執(zhí)行時機,得結合 Vue 的 diff 過程來看。

在 diff 過程中,會對同級相同類型的節(jié)點進行對比更新,實際上就是對老的虛擬 DOM 節(jié)點( oldVnode )和新的虛擬 DOM 節(jié)點( newVnode )進行對比更新。

如果是第一次渲染,那么 oldVnode 會被設置成一個空節(jié)點( emptyVnode ),方便復用對比更新邏輯。

這個新老虛擬節(jié)點的比對過程,自然也包括虛擬節(jié)點上的指令的比對。在對指令進行對比的時候,會確保虛擬節(jié)點對應的真實 DOM 節(jié)點已經(jīng)創(chuàng)建出來了。

創(chuàng)建流程

如果是創(chuàng)建流程,那么就是 oldEmptyVnode 和 newVnode 對比,其中 newVnode 上面已經(jīng)關聯(lián)好了相應的 DOM 節(jié)點,此時直接就調(diào)用 bind 鉤子函數(shù)了。

然后在 DOM 節(jié)點插入父 DOM 節(jié)點之后,就調(diào)用 inserted 鉤子函數(shù)。

bind 只會在指令和 DOM 節(jié)點綁定的時候才會被調(diào)用。

inserted 只會在 DOM 節(jié)點插入到父 DOM 節(jié)點時才會被調(diào)用。

更新流程

如果某個組件數(shù)據(jù)發(fā)生了變化,需要調(diào)用 render 方法重新渲染,那么這就會引起一個在組件范圍內(nèi)的更新流程,該組件下的虛擬節(jié)點樹(直觀感受就是組件模板里面寫的那些節(jié)點)就會進行新老比對,走 diff 流程。

如果碰到帶指令的 VNode ,就要進行指令 diff 了,在這個過程中就會調(diào)用 updated 鉤子函數(shù)。

然后執(zhí)行后續(xù) VNode 比對,等都 diff 完了之后,就會立即調(diào)用之前帶指令 VNode 的 componentUpdated 鉤子函數(shù)了。

解綁銷毀

在指令與 DOM 節(jié)點解除綁定的時候,會調(diào)用 unbind 鉤子函數(shù)。

實例

流程理論描述總是蒼白的,有時候很難讓人快速理解,所以此處用一些簡單的例子進行說明。

基本例子

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 ,然后在每個鉤子函數(shù)里面都打印各自的一些內(nèi)容。

在 Test 組件中,有一個 span 元素使用了 dir 指令,并且該元素受 shouldBind 變量控制,如果該變量為假值,那么指令和 DOM 元素就會解除綁定。組件模板中訪問了 name ,方便通過改變 name 引起組件重新 render 。

執(zhí)行上述代碼,可以看到如下輸出:

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 元素綁定,所以會調(diào)用 bind 鉤子函數(shù),輸出 dir bind 。同時在 bind 的時候, span 元素還沒有被插入父元素( div )中,因此輸出了 false 。

在 span 元素插入父元素( div )之后,會馬上調(diào)用 inserted 鉤子函數(shù),輸出 dir insertedtrue

過了一秒之后, name 值變?yōu)?yibuyisheng ,觸發(fā)了 Test 組件調(diào)用 render ,觸發(fā) diff 流程。在做 span 元素對應的新老虛擬節(jié)點對比的時候,就會調(diào)用 dir 指令的 update 鉤子函數(shù),輸出 dir update ,但是此時 name 數(shù)據(jù)還沒有更新到 DOM 樹中去,因此拿到的 span 的 textContent 還是 ----- ,輸出 ----- 。

同步 diff 走完子孫虛擬節(jié)點之后, name 的值已經(jīng)被更新到 DOM 樹中去了,此時會調(diào)用 componentUpdated 鉤子函數(shù),輸出 dir componentUpdated----- yibuyisheng

再過一秒之后, shouldBind 變?yōu)?false ,觸發(fā) Test 組件的 render ,繼而走 diff 流程。在 span 元素的指令 diff 過程中,發(fā)現(xiàn) span 元素應當被移除,因此會解綁 span 元素和指令,所以會調(diào)用 dir 的 unbind 鉤子函數(shù),輸出 dir unbind ,同時因為 span 元素已經(jīng)被移除了,所以也不存在父元素了,最終輸出 false 。

DOM 節(jié)點復用

指令鉤子函數(shù)的這種機制,結合 diff 算法中的 DOM 節(jié)點復用,會有一點意想不到的結果:

<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

從輸出結果中發(fā)現(xiàn), this.$el.firstElementChild.__id 的值全部是 1 ,說明整個過程只有一個 div 元素, div 元素被復用了。

示例中,對第一個 div 元素加了一個 a="1" 屬性,主要是為了保證兩個 div 虛擬節(jié)點能被判定為同類型的虛擬節(jié)點。

在初始化的時候, someCondition 為 true ,對應模板中的 v-if 分支生效。

一秒后, someCondition 為 false ,對應模板中的 v-else 分支生效,此時因為兩個 div 虛擬節(jié)點是同類型的,因此會復用之前生成的 div DOM 元素,同時將 v-some-directive 指令與該元素關聯(lián)起來,因此輸出了第一組 bindinserted 。

再過一秒后, someCondition 為 true ,對應模板中 v-if 分支生效, v-else 分支生效,同樣復用之前的 div DOM 元素,同時將 v-some-directive 與 div DOM 元素解綁,調(diào)用指令的 unbind 鉤子函數(shù),輸出 unbind 。

再過一秒, someCondition 變?yōu)?true ,重復前述過程。

這里要注意,在官方文檔中,關于 inserted 鉤子函數(shù)的描述是這樣的:

inserted:被綁定元素插入父節(jié)點時調(diào)用 (僅保證父節(jié)點存在,但不一定已被插入文檔中)。

從上面這個例子可以看出,這句描述是非常不嚴謹?shù)?,因為在第三秒的時候,并沒有發(fā)生被綁定元素被插入父節(jié)點的過程,但是卻調(diào)用了 inserted 鉤子函數(shù)。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容