Appearance
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:
js
// 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');小提醒 - Hooks function 與 this
Hooks function 請不要加在 methods 屬性裡面,且由於需透過 this 存取實體,所以與 mehtods 同樣也無法使用箭頭函數。
小提醒 - unmount 卸載元件
若在 Vue.createApp 時直接接上 .mount(...),則無法透過 vm.unmount() 來卸載元件:
js
const vm = Vue.createApp({
// 略
}).mount('#app');
// Error: "vm.unmount is not a function"
vm.unmount();需要改為以下寫法方可順利執行:
js
const vm = Vue.createApp({
// 略
});
// mount
vm.mount('#app');
// It's ok.
vm.unmount();以 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 屬性來處理:
js
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');要注意的是,前面講到 DOM 的更新動作在 Vue.js 裡是非同步執行的,當 setter 偵測到狀態被更新時, 就會啟動一個排隊的隊伍 (Queue),並且對同一個事件循環 (Event Loop) 內發生的所有變更進行緩衝,
這樣做的好處,是若同一個 watch 在短時間內被多次觸發,它只會被送進等待隊伍一次,可以省去多餘重複的計算次數, 直到下一個事件循環 (Vue 官方稱 tick) 才會刷新重整在等待隊伍內的任務,更新並且同步 Vue 實體內的 DOM。
這樣的說法可能不容易理解,直接來看個例子:
html
<div class="messages">
<div v-for="m in messages">{{ m }}</div>
</div>
<input type="text"
placeholder="輸入任意文字後按下 enter 鍵"
v-model.trim="msg"
@keydown.enter="addToMessages">js
const vm = Vue.createApp({
data () {
return {
msg: '',
messages: ['Hello', 'Vue.js', '好棒棒']
}
},
methods: {
addToMessages() {
this.messages.push(this.msg);
this.msg = '';
}
}
});這是一個相當常見的案例,當使用者按下 enter 鍵時會將輸入的文字送進 data 的 messages 陣列。
如果我們希望在這個訊息框加上一個功能:當訊息增加的時候,訊息列表的捲軸自動捲至最底。
這個功能並不困難,我們只需要改寫 addToMessages :
js
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;
}這個功能看起來只要加上兩行程式即可達成,但是各位讀者不妨試試是否正常運作。
各位讀者應該會發現,輸入訊息後雖然 messages 的內容增加了,但是捲軸長度始終與 DOM 實際的 scrollHeight 有一行的落差。
原因前面說過,雖然我們 data 裡的 messages 已經更新了, 但是執行到 el.scrollTop = el.scrollHeight 的時候,畫面還未更新,所以這時的 el.scrollHeight 總是會取得更新前的數字。
要解決這個問題, Vue.js 也提供了對應的方法,就是那個大家可能都聽過但卻不太熟悉的 vm.$nextTick()。
透過 Vue.js 的 $nextTick 可以確保裡面 callback function 執行的任務,會等待畫面都更新結束後才執行:
js
addToMessages () {
this.messages.push(this.msg);
this.msg = '';
// 等待畫面更新後再即時抓取元素屬性
this.$nextTick(() => {
const el = this.$el.querySelector('.messages');
el.scrollTop = el.scrollHeight;
});
}修正後的版本:
簡單來說, $nextTick 的使用時機在當狀態更新時,需要手動存取 DOM 的時候,需要確保畫面都已更新完成。
雖說 Vue.js 開發大多都將關注點放在狀態管理上, 但有時候我們還是需要自行處理 DOM API,這時 $nextTick 的重要性便不言而喻。
小提醒
有關 JavaScript 的 Event Loop 與同步/非同步的機制,可以參考拙作 【談談 JavaScript 的 setTimeout 與 setInterval】 與【 重新認識 JavaScript: Day 26 同步與非同步 】。
上面兩篇剛好都有收錄在【0 陷阱!0 誤解!8 天重新認識 JavaScript】一書中喔
Vue 實體的銷毀
Vue 實體在銷毀的時候,會先觸發 beforeUnmount Hook,然後將實體內的各種事件、狀態的 watcher 、子元件 (如果有) 通通卸除, 完成後觸發 unmount Hook,結束這個實體的一生。
此時,我們就再也無法對這個實體進行任何操作了。