# 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 實體已建立,狀態與事件已初始化完成 (propdatacomputed 等屬性已建立,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 名稱除了 beforeCreatecreated 由新的 setup() 所取代, 以及元件銷毀的 beforeDestroydestroyed 改為 onBeforeUnmountonUnmounted 之外, 多數都是在原有名稱加上 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');
1
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();
1
2
3
4
5
6

需要改為以下寫法方可順利執行:

const vm = Vue.createApp({
// 略
});

// mount
vm.mount('#app');

// It's ok.
vm.unmount();
1
2
3
4
5
6
7
8
9

以 Vue.js 的實體來說,由生到死我們可以分為三個階段:

生命週期示意圖
試一試

# Vue 實體的建立

Vue 的實體從建立、掛載到渲染至各位的瀏覽器畫面上,會經歷這幾個階段: beforeCreatecreatedbeforeMountmounted

beforeCreate 期間,Vue 實體剛被建立,狀態與事件都尚未初始化,此時我們還無法取得 datapropcomputed 等屬性。

直到 Vue 實體內的各種屬性、狀態的偵測 (前個小節所提到的 gettersetter ) 都已經初始化完成後,這才進入了 created 階段。 換句話說,若是我們需要透過遠端 API 來取得資料,至少得在 created 階段以後才能存取實體的 data 屬性。

created 階段完成後,Vue 的實體尚未與模板結合綁定,這個時候 Vue 實體會去尋找 el (2.x) 指定的節點 或 template 屬性來作為元件的模板。

而到了 Vue 3.0 則是需要在執行 vm.mount(...) 之後才會開始 beforeCreate 的階段。

小提醒

Vue 的單一元件檔 (Single File Component, SFC) 則無需加入 eltemplate 屬性,它會自動將 .vue 檔案內 <template> 標籤的內容作為模板。

取得了模板內容,並進行編譯後,會先進入 beforeMount 階段。 就在 Vue.js 的實體將網頁上實際節點的內容替換完成後,這才進入了 mounted,也就是各位看到的最終結果。

以 jQuery 來比喻,這階段就像是 Vue 實體的 DOM Ready。

直到 mounted 階段, Vue.js 才正式將網頁上的 DOM 節點、事件都綁定至 Vue 的實體。 也就是說,如果我們基於某些原因需要手動操作 DOM API,如 querySelectoraddEventlistener 等, 最好在 mounted 階段完成後進行操作,以免操作的 DOM 節點被 Vue.js 替換掉。

# 狀態的更新與畫面的同步

在 Vue 實體生命週期中,我們可以透過 beforeUpdateupdated 兩個 Hooks 來觀察到實體狀態的更新, 而它們的執行會根據模板的畫面更新前/後時機來觸發。

可是,如果只需要觀察 datacomputed 內某個狀態的時候,使用 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');
1
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">
1
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 = '';
    }
  }
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

這是一個相當常見的案例,當使用者按下 enter 鍵時會將輸入的文字送進 datamessages 陣列。

如果我們希望在這個訊息框加上一個功能:當訊息增加的時候,訊息列表的捲軸自動捲至最底。

這個功能並不困難,我們只需要改寫 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;
}
1
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;
  });
}
1
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,結束這個實體的一生。

此時,我們就再也無法對這個實體進行任何操作了。

Last Updated: 1/7/2021, 1:10:34 PM