在開發通常的業務來講,不須要知道 Vue 中鉤子函數過多的執行細節。可是若是你想寫出足夠穩健的代碼,或者想開發一些通用庫,那麼就少不了要深刻了解各類鉤子的執行時機了。javascript
先直接看一個例子: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 事件的捕獲和冒泡過程來看:算法
同時,能夠看到,在初始化流程、 update 流程和銷燬流程中,子級的相應聲明週期方法都是在父級相應週期方法之間調用的。好比子級的初始化鉤子函數( beforeCreate 、 created 、 mounted )都是在父級的 created 和 mounted 之間調用的,這實際上說明等到子級準備好了,父級纔會將本身掛載到上一層 DOM 樹中去,從而保證界面上不會閃現髒數據。bash
充分理解這個調用過程是頗有必要的,好比有下面兩個很是常見的場景:app
在對話框組件的實現中,爲了方便處理浮層遮蓋問題,每每會將浮層根元素放置到 body 元素下面,而不是讓其保持在書寫對話框組件所在的位置。同時須要作一個浮層的層疊順序管理,正確處理對話框相互之間的視覺覆蓋關係。函數
爲了達到這個效果,能夠在對話框組件的 created 鉤子函數中向全局層疊管理器註冊本身,而後拿到本身的 z-index 值,而後在 mounted 的時候將浮層根元素插入到 body 元素下。ui
有不少這種類型的組件,好比 Select 和 Option 、 Tab 和 TabItem 、 Table 和 TableRow 等等。通常狀況下,會採用子級組件向父級組件註冊的方式來實現這種依賴關係,由於在子級的鉤子函數中,能夠明確地知道必定存在父級組件,因此往上查找起來會很是方便。this
在 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 inserted
和 true
。
過了一秒以後, 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
。
指令鉤子函數的這種機制,結合 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 指令與該元素關聯起來,所以輸出了第一組 bind
、 inserted
。
再過一秒後, someCondition 爲 true ,對應模板中 v-if 分支生效, v-else 分支生效,一樣複用以前的 div DOM 元素,同時將 v-some-directive 與 div DOM 元素解綁,調用指令的 unbind 鉤子函數,輸出 unbind
。
再過一秒, someCondition 變爲 true ,重複前述過程。
這裏要注意,在官方文檔中,關於 inserted 鉤子函數的描述是這樣的:
inserted
:被綁定元素插入父節點時調用 (僅保證父節點存在,但不必定已被插入文檔中)。
從上面這個例子能夠看出,這句描述是很是不嚴謹的,由於在第三秒的時候,並無發生被綁定元素被插入父節點的過程,可是卻調用了 inserted 鉤子函數。