# 1-7 元件的生命週期與更新機制
雖然還沒有正式開始說明 Vue.js 元件系統 (component system) 的部分, 但如果各位讀者是從最前面一路看到這裡,其實我們已經在寫元件了。
我們前面說過,一個 Vue.js 的網頁應用程式是由各種大小元件組合而成, 而每個 Vue 的實體物件,實際上就是一個元件,而每個 Vue 元件從建立到被銷毀,都有它的生命週期階段。
那麼在這個小節中,將為各位讀者說明,Vue.js 是如何從 JavaScript 的實體物件到最後各位所看到的網頁應用, 從建立到銷毀的過程。
# 生命週期與 Hooks function
如同生物一般, Vue 的實體物件從建立、掛載、更新,到銷毀移除,這一連串的過程,我們將它稱作生命週期。 在這個過程中, Vue.js 提供了開發者在這些週期階段做對應處理的 callback function, 這些 callback function 我們就稱它叫生命週期的 Hooks function。
Vue.js 提供的 Hooks function 主要有下列幾種,這裡也將對應至 Vue 3.x Composition API 的版本一並列出給讀者們做對照:
Hooks 名稱 (Vue 2.x/3.x) | Hooks 名稱 (對應 Vue 3.0 Composition API) | 說明 |
---|---|---|
beforeCreate | setup() | Vue 實體被建立,狀態與事件都尚未初始化 |
created | setup() | Vue 實體已建立,狀態與事件已初始化完成 (prop 、data 、computed 等屬性已建立,vm.$el 屬性無法使用 ) |
beforeMount | onBeforeMonut | Vue 實體尚未與模板 (DOM 節點) 綁定 |
mounted | onMounted | Vue 實體與掛載完成, el 的目標 DOM 被 $el 所替換 (可以視作 jQuery 的 Ready) |
beforeUpdate | onBeforeUpdate | 當狀態被變動時,畫面同步更新前 |
updated | onUpdated | 當狀態被變動時,畫面已同步更新完成 |
beforeDestroy (2.x) | onBeforeUnmount | Vue 實體物件被銷毀前 |
beforeUnmount (3.0) | onBeforeUnmount | Vue 實體物件被銷毀前 |
destroyed (2.x) | onUnmounted | Vue 實體物件被銷毀完畢 |
unmounted (3.0) | onUnmounted | Vue 實體物件被銷毀完畢 |
errorCaptured | onErrorCaptured | 子/孫代元件的錯誤被捕獲時觸發 |
activated | -- | Vue 元件被啟動時觸發,搭配 keep-alive 使用 |
deactivated | -- | Vue 元件被解除時觸發,搭配 keep-alive 使用 |
小提醒: Composition API 與 Hooks function
Vue Composition API 是 Vue.js 3.0 開始提供的新特性,Vue.js 3.0 針對多數 2.x 的語法提供了向下相容,所以在本節介紹 Vue.js 2.x Options API 的生命週期 Hooks 到了 3.0 依然可以繼續使用。
Composition API 的 Hook 名稱除了 beforeCreate
與 created
由新的 setup()
所取代,
以及元件銷毀的 beforeDestroy
與 destroyed
改為 onBeforeUnmount
與 onUnmounted
之外,
多數都是在原有名稱加上 on
來表示。
關於 Vue Composition API 的詳細內容,往後還會有專門的章節來說明。
使用方式也很直觀,就在 Vue 實體的屬性裡加入對應名稱的 hooks function, 這樣 Vue 實體進行至不同生命週期的階段時,就會自動觸發這個 hooks function:
// for Vue 3.x
const vm = Vue.createApp({
data () {
return {
msg: 'Hello Vue.js!'
}
},
created () {
console.log('created');
},
mounted () {
console.log('mounted');
},
unmounted () {
console.log('unmounted');
},
});
// 注意! 若未執行 mount 動作,
// 則後續所有的 lifecycle hook 都將不會繼續執行!
vm.mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
小提醒 - Hooks function 與 this
Hooks function 請不要加在 methods
屬性裡面,且由於需透過 this
存取實體,所以與 mehtods
同樣也無法使用箭頭函數。
小提醒 - unmount 卸載元件
若在 Vue.createApp
時直接接上 .mount(...)
,則無法透過 vm.unmount()
來卸載元件:
const vm = Vue.createApp({
// 略
}).mount('#app');
// Error: "vm.unmount is not a function"
vm.unmount();
2
3
4
5
6
需要改為以下寫法方可順利執行:
const vm = Vue.createApp({
// 略
});
// mount
vm.mount('#app');
// It's ok.
vm.unmount();
2
3
4
5
6
7
8
9
以 Vue.js 的實體來說,由生到死我們可以分為三個階段:
# Vue 實體的建立
Vue 的實體從建立、掛載到渲染至各位的瀏覽器畫面上,會經歷這幾個階段: beforeCreate
、 created
、 beforeMount
、 mounted
。
在 beforeCreate
期間,Vue 實體剛被建立,狀態與事件都尚未初始化,此時我們還無法取得 data
、 prop
、 computed
等屬性。
直到 Vue 實體內的各種屬性、狀態的偵測 (前個小節所提到的 getter
與 setter
) 都已經初始化完成後,這才進入了 created
階段。
換句話說,若是我們需要透過遠端 API 來取得資料,至少得在 created
階段以後才能存取實體的 data
屬性。
當 created
階段完成後,Vue 的實體尚未與模板結合綁定,這個時候 Vue 實體會去尋找 el
(2.x) 指定的節點 或 template
屬性來作為元件的模板。
而到了 Vue 3.0 則是需要在執行 vm.mount(...)
之後才會開始 beforeCreate
的階段。
小提醒
Vue 的單一元件檔 (Single File Component, SFC) 則無需加入 el
或 template
屬性,它會自動將 .vue
檔案內 <template>
標籤的內容作為模板。
取得了模板內容,並進行編譯後,會先進入 beforeMount
階段。
就在 Vue.js 的實體將網頁上實際節點的內容替換完成後,這才進入了 mounted
,也就是各位看到的最終結果。
以 jQuery 來比喻,這階段就像是 Vue 實體的 DOM Ready。
直到 mounted
階段, Vue.js 才正式將網頁上的 DOM 節點、事件都綁定至 Vue 的實體。
也就是說,如果我們基於某些原因需要手動操作 DOM API,如 querySelector
或 addEventlistener
等,
最好在 mounted
階段完成後進行操作,以免操作的 DOM 節點被 Vue.js 替換掉。
# 狀態的更新與畫面的同步
在 Vue 實體生命週期中,我們可以透過 beforeUpdate
與 updated
兩個 Hooks 來觀察到實體狀態的更新,
而它們的執行會根據模板的畫面更新前/後時機來觸發。
可是,如果只需要觀察 data
或 computed
內某個狀態的時候,使用 beforeUpdate
又顯得太麻煩,
這個時候我們就可以透過 watch
屬性來處理:
const vm = Vue.createApp({
data () {
return {
msg: 'Hello Vue.js!'
}
},
watch: {
// 當 this.msg 被更新時觸發
msg (val, oldValue) {
console.log(`新的 msg: ${val}`);
console.log(`舊的 msg: ${oldValue}`);
}
}
}).mount('#app');
2
3
4
5
6
7
8
9
10
11
12
13
14
要注意的是,前面講到 DOM 的更新動作在 Vue.js 裡是非同步執行的,當 setter
偵測到狀態被更新時,
就會啟動一個排隊的隊伍 (Queue),並且對同一個事件循環 (Event Loop) 內發生的所有變更進行緩衝,
這樣做的好處,是若同一個 watch
在短時間內被多次觸發,它只會被送進等待隊伍一次,可以省去多餘重複的計算次數,
直到下一個事件循環 (Vue 官方稱 tick
) 才會刷新重整在等待隊伍內的任務,更新並且同步 Vue 實體內的 DOM。
這樣的說法可能不容易理解,直接來看個例子:
<div class="messages">
<div v-for="m in messages">{{ m }}</div>
</div>
<input type="text"
placeholder="輸入任意文字後按下 enter 鍵"
v-model.trim="msg"
@keydown.enter="addToMessages">
2
3
4
5
6
7
8
const vm = Vue.createApp({
data () {
return {
msg: '',
messages: ['Hello', 'Vue.js', '好棒棒']
}
},
methods: {
addToMessages() {
this.messages.push(this.msg);
this.msg = '';
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
這是一個相當常見的案例,當使用者按下 enter 鍵時會將輸入的文字送進 data
的 messages
陣列。
如果我們希望在這個訊息框加上一個功能:當訊息增加的時候,訊息列表的捲軸自動捲至最底。
這個功能並不困難,我們只需要改寫 addToMessages
:
addToMessages () {
this.messages.push(this.msg);
this.msg = '';
// 透過 this.$el 取得實體綁定後的 DOM
const el = this.$el.querySelector('.messages');
// 將 el.scrollTop 指定為捲軸的高度 el.scrollHeight
el.scrollTop = el.scrollHeight;
}
2
3
4
5
6
7
8
9
這個功能看起來只要加上兩行程式即可達成,但是各位讀者不妨試試是否正常運作。
各位讀者應該會發現,輸入訊息後雖然 messages
的內容增加了,但是捲軸長度始終與 DOM 實際的 scrollHeight
有一行的落差。
原因前面說過,雖然我們 data
裡的 messages
已經更新了,
但是執行到 el.scrollTop = el.scrollHeight
的時候,畫面還未更新,所以這時的 el.scrollHeight
總是會取得更新前的數字。
要解決這個問題, Vue.js 也提供了對應的方法,就是那個大家可能都聽過但卻不太熟悉的 vm.$nextTick()
。
透過 Vue.js 的 $nextTick
可以確保裡面 callback function 執行的任務,會等待畫面都更新結束後才執行:
addToMessages () {
this.messages.push(this.msg);
this.msg = '';
// 等待畫面更新後再即時抓取元素屬性
this.$nextTick(() => {
const el = this.$el.querySelector('.messages');
el.scrollTop = el.scrollHeight;
});
}
2
3
4
5
6
7
8
9
10
修正後的版本:
簡單來說, $nextTick
的使用時機在當狀態更新時,需要手動存取 DOM 的時候,需要確保畫面都已更新完成。
雖說 Vue.js 開發大多都將關注點放在狀態管理上,
但有時候我們還是需要自行處理 DOM API,這時 $nextTick
的重要性便不言而喻。
小提醒
有關 JavaScript 的 Event Loop 與同步/非同步的機制,可以參考拙作 【談談 JavaScript 的 setTimeout 與 setInterval (opens new window)】 與【 重新認識 JavaScript: Day 26 同步與非同步 (opens new window) 】。
上面兩篇剛好都有收錄在【0 陷阱!0 誤解!8 天重新認識 JavaScript】一書中喔
# Vue 實體的銷毀
Vue 實體在銷毀的時候,會先觸發 beforeUnmount
Hook,然後將實體內的各種事件、狀態的 watcher
、子元件 (如果有) 通通卸除,
完成後觸發 unmount
Hook,結束這個實體的一生。
此時,我們就再也無法對這個實體進行任何操作了。