理清 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 鉤子函數。

相關文章
相關標籤/搜索