是的,這個篇幅會用來聊一下生命週期當中,比較奇怪的事情。薛丁格 我就不解釋了,程式執行其實並沒有像是量子力學的那種疊加狀態,只是,當 Vue 的元件趨於複雜,再搭配上 Vue Router 的時候,你的某些生命週期中的勾子,就會有你所不知道的狀態。


最終狀態

有些時候你可能會遇到一個情況,當我們使用 console.log() 去印出某個物件的時候,你會發現他是有東西的。然後你如果印了某個物件的屬性,例如說 HelloKitty.foo 結果是 undefined

不要問我為什麼,這是 JavaScript 的 FEATURE,絕對不是 BUG。

我們先來看兩張執行結果:

差異在於,App.vuemounted 的時候,我們用 this.$refs 來取得放在 App.vue 裡面的元件集合。結果你會看到第二個卻發了一個空集合出來。這到底是怎麼回事呢?

這是第一個執行結果程式碼,我把他簡化一下,

import HelloKitty from '@/components/HelloKitty.vue'

export default {
  name: 'App',
  components: {
    HelloKitty
  },
  mounted () {
    console.log('App mounted')
    console.log(this.$refs)
  },
  updated () {
    console.log('App updated')
    console.log(this.$refs)
  }
}

這是第二個執行結果程式碼,

let HelloKitty = () => import('@/components/HelloKitty.vue')

export default {
  name: 'App',
  components: {
    HelloKitty
  },
  mounted () {
    console.log('App mounted')
    console.log(this.$refs)
  },
  updated () {
    console.log('App updated')
    console.log(this.$refs)
  }
}

只差在 import 的用法,還記得我們之前說過 import 其實是一個 Promise 嗎?而第二種寫法,你會在 Vue Router 裡面看到這種方法,官方也有說明這種懶加載的方式,另外,其實 Vue 元件也有非同步載入的方式(我們之前提過了):

Lazy Loading Routes

Dynamic & Async Components

Component 魔術方法 Day 4

所以,你會發現你原本要在 mounted 做的事情,會因為 this.$refs 找不到元件,所以你會看到一個空物件出現。同樣的道理,如果你在 <router-view> 當中使用 ref 屬性,是的,他也是可以用 ref 的:

<template>
  <section>
    <router-view ref="RouterView"></router-view>
  </section>
</template>

然後,你以為 mounted 不行,就都放到 updated 總會觸發,當然,事情不是憨人想的那麼簡單:

要重現這個錯誤很簡單,只需要:

  1. 打開首頁。
  2. 點入 Hello 連結。
  3. 「上一頁」。

你就會拿到 this.$refs.RouterView 是存在的,但是他等於 undefined,因為你「回到首頁」之後,你的 <router-view> 本身的元件已經被銷毀了,但是對於 App 來說,你所設定的 ref 還是存在的。

所以他一共出現了三種狀態,不存在,存在,存在卻無定義。

我們叫做「薛丁格」物件。

所以,如果你想要用 console.log() 的方法來測試,不要印出你的 目標物件,印出你的 物件特徵值 ,他一定會是在那個當下的 最終結果,並不會因為 JavaScript 的「傳值」或是「傳址」或是 Pass by Sharing 的關係,導致看似那個結果好像存在又好像不存在的樣貌。

我們叫做「薛丁格」物件 (住口)


為何好像我很容易遇到這種奇怪狀況?

因為我動不動就用動態載入啊 (哈哈你看看你)

對不起!

撇開我們之前提到的一些比較奇怪的載入方式,基本上只要是屬於懶加載,你的元件都不一定會在根元件或是父元件已經 mounted 之後出現。所以,如果你使用 Vue 在開發元件時,當使用第三方,爾或是用你自己的元件,應該,或多或少膝蓋都曾中箭過:

import SomeOtherComponent from '@/components/Some/Other/Component.vue'

export default {
  name: 'App',
  components: {
    SomeOtherComponent
  },
  mounted () {
    this.$el.querySelector('.some-other-component-dom').addEventListener('dragstart', (evt) => {
      evt.preventDefault()
      
      console.log(evt);
    }, false)
  }
}

然後你的 App.vue 告訴你:

TypeError: Cannot read property 'addEventListener' of null

你一定會覺得奇怪,我的父元件不是已經 mounted 進去了嗎,為何會選不到 DOM 呢?這就是上述提到加載方式造成的。因為 Vue 本身也是走虛擬節點( VNode )的方式,也就是所謂的虛擬 DOM,所以在那個當下,其實你的元件 並沒有 真正進入 瀏覽器的 DOM 結構樹當中。

所以,原生方法取不到自然也是合理。


薛丁格的 OOXX

你要套用到物件、生命週期、路由、Vuex 或甚至是 DOM 都會通。你要避免這種其實存在但是又不存在的狀態,你必須要明確的知道幾件事情:

  • 關於元件的部分:
    1. 確認你的元件與其元件鏈上,沒有懶加載。
    2. 請注意元件 v-if 系列操作的特性,與 v-for 搭配的問題點。
    3. 請注意元件 :is 魔術方法的使用時機。
    4. 請注意元件 $slot 所傳入的 DOM。
    5. 請留意 Vue.component 全域元件設定時機。
    6. 請留意放在 <keep-alive> 裡面的元件。
  • 關於路由的部分:
    1. 不會影響元件自身生命週期。
    2. 請留意 getMatchedComponents() 方法,可能會取得空陣列。
    3. 注意非同步處理的 Actions 與 commit, dispatch 時機。
    4. 關於 vue-router 外面的兩三事
  • 關於 Vuex 的部分:
    1. Vuex 的五十道陰影
    2. Vuex 2.0 關於 plugins 的事情
    3. state 的預設值給好給滿會比較沒事。

以上是一點點經驗談,希望能有所幫助。如果要舉例子應該會被說是充版面,你們自己可以慢慢做實驗,然後在底下留言給我 500 字心得,或是 Email 給我也是可以,我會看(欸不對)。


之所以會在生命週期中遇到怪事,主要原因還是在於元件的拆分方式,以及你所載入與使用的時機。像是上述提到的 v-if 系列操作,還記得 v-if 的特性嗎?他是會 銷毀整個 DOM ,不僅僅是在虛擬節點( VNode )上消除,連瀏覽器的結構樹都會被 移除

所以,當你針對你的元件下 v-if 之前,請先思考 3 秒。

太短是不是,那 10 秒。

v-if 操作有存在跟不存在,使用前請詳閱公開說明書。

然後跟 v-if 很像的就是 :is,但是他更棒, 會有三種疊加狀態 (你不要這樣)。跟 v-if 一樣的就是,無論是虛擬節點( VNode )或是瀏覽器的結構樹,都會被銷毀。但是,如果像上述有指定 ref,則 ref 會出現一個 undefined 的值。

但,由於 :is 可以搭配 <keep-alive> 使用,所以,一旦被放到 <keep-alive> 裡面的話,你的元件就不會在虛擬節點被消除,而是會被 快取 起來。這個時候,你的元件生命週期會多兩個勾子可以用,

  • activated:is 顯示該元件時觸發。
  • deactivated:is 離開該元件時觸發。

然後有一件事情要提醒你,

任何在 <keep-alive> 當中的元件,mounted 會一直被觸發。
任何在 <keep-alive> 當中的元件,mounted 會一直被觸發。
任何在 <keep-alive> 當中的元件,mounted 會一直被觸發。

不要問,很可怕!

至於 Slot 的部分,記得他是一個虛擬節點( VNode ),同時也會被瀏覽器渲染在結構樹當中,但請記得搭配 v-if 的使用會產生的問題,在 Slot 上面也一樣會發生。


小結

原本想再舉一些關於 Vue Router 跟 Vuex 的例子,不過我手頭上的範例都有點太生硬,而且使用情境太過於機歪,所以想想還是算了。後續的文章中應該有機會提到,這邊就不要荼毒大家了。

瓦斯爐煮飯要多放一點水,米心才不會太硬(不是這種飯粒)。

ITHome 鐵人賽同步刊登 薛丁格的生命週期 Day 12