在開發(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 inserted 和 true 。
過了一秒之后, 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)起來,因此輸出了第一組 bind 、 inserted 。
再過一秒后, 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ù)。