是的,這個篇幅會用來聊一下生命週期當中,比較奇怪的事情。薛丁格 我就不解釋了,程式執行其實並沒有像是量子力學的那種疊加狀態,只是,當 Vue 的元件趨於複雜,再搭配上 Vue Router 的時候,你的某些生命週期中的勾子,就會有你所不知道的狀態。
最終狀態
有些時候你可能會遇到一個情況,當我們使用 console.log()
去印出某個物件的時候,你會發現他是有東西的。然後你如果印了某個物件的屬性,例如說 HelloKitty.foo
結果是 undefined
。
不要問我為什麼,這是 JavaScript 的 FEATURE,絕對不是 BUG。
我們先來看兩張執行結果:
差異在於,App.vue
被 mounted
的時候,我們用 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 元件也有非同步載入的方式(我們之前提過了):
所以,你會發現你原本要在 mounted
做的事情,會因為 this.$refs
找不到元件,所以你會看到一個空物件出現。同樣的道理,如果你在 <router-view>
當中使用 ref
屬性,是的,他也是可以用 ref
的:
<template>
<section>
<router-view ref="RouterView"></router-view>
</section>
</template>
然後,你以為 mounted
不行,就都放到 updated
總會觸發,當然,事情不是憨人想的那麼簡單:
要重現這個錯誤很簡單,只需要:
- 打開首頁。
- 點入 Hello 連結。
- 「上一頁」。
你就會拿到 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 都會通。你要避免這種其實存在但是又不存在的狀態,你必須要明確的知道幾件事情:
- 關於元件的部分:
- 確認你的元件與其元件鏈上,沒有懶加載。
- 請注意元件
v-if
系列操作的特性,與v-for
搭配的問題點。 - 請注意元件
:is
魔術方法的使用時機。 - 請注意元件
$slot
所傳入的 DOM。 - 請留意
Vue.component
全域元件設定時機。 - 請留意放在
<keep-alive>
裡面的元件。
- 關於路由的部分:
- 不會影響元件自身生命週期。
- 請留意
getMatchedComponents()
方法,可能會取得空陣列。 - 注意非同步處理的 Actions 與
commit
,dispatch
時機。 - 關於 vue-router 外面的兩三事
- 關於 Vuex 的部分:
- Vuex 的五十道陰影
- Vuex 2.0 關於 plugins 的事情
- 把
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 的例子,不過我手頭上的範例都有點太生硬,而且使用情境太過於機歪,所以想想還是算了。後續的文章中應該有機會提到,這邊就不要荼毒大家了。
瓦斯爐煮飯要多放一點水,米心才不會太硬(不是這種飯粒)。