我在先前的篇幅當中,有提到生命週期與路由的關係。我們這一個篇章,就將路由與生命週期之間的事情,做一個全面性的剖析。如果你之前有稍稍稍稍微留意我的部落格,應該會看過我曾經碎念過 Router 與生命週期之間的事情。
所以我就不跪求心理陰影面積了。
生命週期與 Hooks(勾子)
我們除了 Vue 原本就有的方法之外,如果另外再加上 Router 所提供的,那麼大家的順序上就很容易搞混,我們先條列一下到底會有哪些方法:
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
beforeDestroy
destroyed
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
以上是可以放在元件( Component )當中的方法,另外還有 beforeEnter
,如果你們還有印象的話,他是放在 Routes 的設定當中。所以這些全部算起來,總共有 12 種 Hooks,那麼這麼多方法到底實際上的差異在哪裡?
所以我們先用單一元件,來試著大亂鬥一下:
import Vue from 'vue';
export default {
name: 'App',
beforeCreate () {
console.log('App beforeCreate.');
},
created () {
console.log('App created.');
},
beforeMount () {
console.log('App beforeMount.');
},
mounted () {
console.log('App mounted.');
},
beforeUpdate () {
console.log('App beforeUpdate.');
},
updated () {
console.log('App updated.');
},
beforeDestroy () {
console.log('App beforeDestroy.');
},
destroyed () {
console.log('App destroyed.');
},
beforeRouteEnter (to, from, next) {
console.log('App beforeRouterEnter.');
next();
},
beforeRouteUpdate (to, from, next) {
console.log('App beforeRouterUpdate.');
next();
},
beforeRouteLeave (to, from, next) {
console.log('App beforeRouterLeave.');
next();
}
}
然後我們的 Route 可能先放幾個連結,我們要大亂鬥的順序大概是這樣:
- 先進入首頁。
- 然後會有三個連結:
Hello
進入/hello
。Kitty
進入/kitty
。HelloKitty
進入/hello/kitty
。- 放一個外部連結,例如前往 Google 首頁。
- 我們依順序點擊上述的網址。
- 然後我們有三個元件對應到上面三個連結,裡面埋入的方法跟 App 一樣:
Hello.vue
,他必須要埋入<router-view>
。Kitty.vue
。HelloKitty.vue
,他是Hello
的子路由。
- 然後我們使用
console.log
來印出結果。
我們的路由大概是這樣:
const router = new Router({
mode: 'history',
routes: [
{
path: '/hello',
name: 'Hello',
component: Hello,
beforeEnter: function (to, from, next) {
console.log('Hello beforeEnter.');
next();
},
children: [
{
path: 'kitty',
name: 'HelloKitty',
component: HelloKitty,
beforeEnter: function (to, from, next) {
console.log('HelloKitty beforeEnter.');
next();
}
}
]
},
{
path: '/kitty',
name: 'Kitty',
component: Kitty,
beforeEnter: function (to, from, next) {
console.log('Kitty beforeEnter.');
next();
}
}
]
});
首先我們先看 App.vue
剛進入首頁的時候:
接著我們進入了 Hello 這個路徑:
再來我們前往 Kitty 這個路徑:
最後我們到 HelloKitty 這個子路徑:
如果我們有一個外部連結,我們試著點擊一下外部連結看看:
根據上面的結果,我們來看看到底這些 Hooks 的執行狀況跟順序是如何。首先,我們從 App.vue
這個最根元件的進入點來看,他只做了四件事情:
beforeCreate
created
beforeMount
mounted
雖然你有在 App.vue
埋入 Router 相關的 Hooks,但是其實是不會被執行的。原因在於,App.vue
本身是所有程序的進入點,而且並沒有符合整個根目錄的任何元件或是執行方法。所以,App.vue
自身是沒有辦法使用 Router 的 Hooks 方法的。
接著,我們看看點擊了 Hello 發生了什麼事情:
- Hello
beforeEnter
- Hello
beforeRouteEnter
- App
beforeUpdate
- Hello
beforeCreate
- Hello
created
- Hello
beforeMount
- Hello
mounted
- App
updated
你可以注意到 App.vue
的 beforeUpdate
與 update
是夾在 Hello.vue
的 Router 與元件的 Hooks 中間,這一點請務必留意。當你在 App.vue
有執行更新的時候,請確認你更新的資料是否跟 Hello.vue
有關,否則可能會拿不到相關的資訊。
然後,我們這次再點擊 Kitty 這個路徑,看看他的執行結果:
- Hello
beforeRouteLeave
- Kitty
beforeEnter
- Kitty
beforeRouteEnter
- App
beforeUpdate
- Kitty
beforeCreate
- Kitty
created
- Kitty
beforeMount
- Hello
beforeDestroy
- Hello
destroyed
- Kitty
mounted
- App
updated
這次要留意的地方,在於 Hello.vue
的 beforeDestory
與 destroyed
是會發生於 Kitty.vue
的 created
, beforeMount
之後 ,且在 mounted
之前 ,這意味著什麼呢?
假設,你在 Kitty.vue
的 created
有做了一些 全域事件 綁定,無論你是使用 EventBus 爾或者是綁定在 window
上面,舉例來說:
window.addEventListener('scroll', function () { ... }, false);
然後,假設你的 Hello.vue
的 created
也有類似的作法,而你在 Hello.vue
的 beforeDestroy
也很乖的將這個事件解除,例如:
window.removeEventListener('scroll');
那麼,依照上述的邏輯,可怕的事情就發生了。你在 Kitty.vue
的 created
所綁定的事件,由於 Hello.vue
的 beforeDestroy
比 Kitty.vue
的 created
慢 執行的關係,所以你所綁定的事件,就這樣被清除掉了。
綁一次不行,你可以綁兩次(欸不對)。
所以,你在解除綁定的時候,不建議使用上述的方式,無論是原生的 removeEventListener
爾或是 EventBus 的 $off
都一樣,最好是使用指定監聽函式( 相同的記憶體位址 )的解除方式。注意,同樣的匿名函式不代表有著同樣的記憶體位址。
請認真閱讀 Kuro 的文章 重新認識 JavaScript: Day 14 事件機制的原理
接著我們來看看,再切換到 HelloKitty 會發生什麼事情:
- Kitty
beforeRouteLeave
- Hello
beforeEnter
- HelloKitty
beforeEnter
- Hello
beforeRouteEnter
- HelloKitty
beforeRouteEnter
- App
beforeUpdate
- Hello
beforeCreate
- Hello
created
- Hello
beforeMount
- HelloKitty
beforeCreate
- HelloKitty
created
- HelloKitty
beforeMount
- Kitty
beforeDestroy
- Kitty
destroyed
- HelloKitty
mounted
- Hello
mounted
- App
updated
看起來超複雜的 ,沒關係你現在放棄還來得及(欸)。我們這邊會發現,由於 HelloKitty.vue
是屬於 Hello.vue
的子路由,所以從上一次的 Hello.vue
的順序當中,又會穿插了 HelloKitty.vue
的方法。
我們這邊插花一下,如果我都是點擊 Hello.vue
的子路由,例如說,我們有一個叫做 HelloWorld 的子路由,跟 HelloKitty 同一層,我們的點擊順序是:
- 先點擊
Hello
。 - 再點擊
Hello > Kitty
。 - 最後點擊
Hello > World
。
- 以下是從 Hello 進入 HelloKitty 的過程:
- Hello
beforeRouteUpdate
- HelloKitty
beforeEnter
- HelloKitty
beforeRouteEnter
- App
beforeUpdate
- Hello
beforeUpdate
- HelloKitty
beforeCreate
- HelloKitty
created
- HelloKitty
beforeMount
- HelloKitty
mounted
- Hello
updated
- App
updated
- Hello
- 以下是從 HelloKitty 進入 HelloWorld 的過程:
- HelloKitty
beforeRouteLeave
- Hello
beforeRouteUpdate
- HelloWorld
beforeEnter
- HelloWorld
beforeRouteEnter
- App
beforeUpdate
- Hello
beforeUpdate
- HelloWorld
beforeCreate
- HelloWorld
created
- HelloWorld
beforeMount
- HelloKitty
beforeDestroy
- HelloKitty
destroyed
- HelloWorld
mounted
- Hello
updated
- App
updated
- HelloKitty
在這裡你會發現,比起從其他路徑進來的過程,如果都是在同一個父層級路由當中變換,那麼 Hello.vue
的 mounted
就不會再被觸發,且,Hello.vue
的 beforeRouteUpdate
會在每次每個子路由離開( beforeRouteLeave
)後,銷毀之前執行。
最後,我們按下外部連結,他連去了 Google,然後你會發現,所有的 Hooks 都不會運作。是的,連同什麼 beforeDestroy
還是 destroyed
都不會觸發。關於這一點請大家特別留意。
接著,我們來看一下「上一頁」這個動作會發生什麼事情?我們從剛剛的順序逆向操作:
- 我們從 Google 上一頁,回到 HelloKitty 頁面。
- 從 HelloKitty 上一頁,會回到 Kitty 頁面。
- 再從 Kitty 回到 Hello 頁面。
- 最後從 Hello 回到首頁。
從這一連串的操作當中,你可以簡單的(?)發現,執行順序大致上沒有太大落差,只是元件的順序有所差異。所以,你在 Router 與生命週期所提供的 Hooks 當中,你必須要理解到這些順序的差異。特別是你有依賴的事件或是元件的時候,必須要特別小心。
另外,Router 所提供的 Hooks,如果不是屬於 Routes 的元件的話,他是不會被觸發的。所以說,如果你在 HelloKitty.vue
當中,引入一個叫做 NotKitty.vue
元件,倘若這個元件 不屬於 你的 Routes 設定元件,那麼你就無法使用 Router 的 Hooks,應該說,你寫了也無效。
import Kitty from '@/components/Kitty.vue';
export default {
name: 'Hello',
components: {
Kitty
},
data () {
return {
foo: '爽 2'
}
},
// 後略...
}
export default {
name: 'Kitty',
data () {
return {
foo: 'Kitty'
}
},
beforeRouteEnter (to, from, next) {
console.log('Kitty beforeRouterEnter.');
next();
},
beforeRouteUpdate (to, from, next) {
console.log('Kitty beforeRouterUpdate.');
next();
},
beforeRouteLeave (to, from, next) {
console.log('Kitty beforeRouterLeave.');
next();
},
// 後略...
}
順序之外的事
當然,順序很重要,就是因為這個順序很重要,所以你必須要留意許多可能會爆炸的地方:
beforeCreate
,beforeMount
,created
順序 父元件 > 子元件。mounted
順序 子元件 > 父元件。- 同父元件,以下事件只會做 1 次:
beforeCreate
created
beforeMount
mounted
beforeEnter
beforeRouteEnter
- 同父元件,完全離開時(不同父元件)才會執行
beforeRouteLeave
。 - 在路由當中「重新整理頁面」的順序不太一樣,以
/hello/kitty
列舉如下:- 先執行
Hello.vue
的beforeEnter
。 - 接著子元件
HelloKitty.vue
的beforeEnter
。 - 然後才是
App.vue
的系列動作:beforeCreate
。created
。beforeMount
。mounted
。
- 接著是
Hello.vue
的beforeRouteEnter
。 - 接著子元件
HelloKitty.vue
的beforeRouteEnter
。 - 再來是
App.vue
的beforeUpdate
。 - 後續就是
Hello.vue
與HelloKitty.vue
與其他元件系列動作:beforeCreated
。created
。beforeMount
。
- 最後在依照路由順序
mounted
,內部元件理論上會優先。 - 最後的最後
App.vue
的updated
。
- 先執行
看到這邊應該想放棄了吧?
放棄雖然可恥,但是有用!
小結
如果說,你覺得這邊已經是很複雜的地方了。那麼恭喜你,可以慢慢放棄沒有關係(欸)。因為在接下來的篇章裡面,我們會來聊聊比較特別的生命週期的狀況,那種你覺得他有 mounted
但是又沒有 mounted
的情況。
薛丁格的生命週期,敬請期待
(可以不要嗎)。