我在先前的篇幅當中,有提到生命週期與路由的關係。我們這一個篇章,就將路由與生命週期之間的事情,做一個全面性的剖析。如果你之前有稍稍稍稍微留意我的部落格,應該會看過我曾經碎念過 Router 與生命週期之間的事情。

重新檢視 lifecycle 與 vue-router

關於 vue-router 外面的兩三事

VueJS 快速入門 Day 1

所以我就不跪求心理陰影面積了。


生命週期與 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 可能先放幾個連結,我們要大亂鬥的順序大概是這樣:

  1. 先進入首頁。
  2. 然後會有三個連結:
    1. Hello 進入 /hello
    2. Kitty 進入 /kitty
    3. HelloKitty 進入 /hello/kitty
    4. 放一個外部連結,例如前往 Google 首頁。
  3. 我們依順序點擊上述的網址。
  4. 然後我們有三個元件對應到上面三個連結,裡面埋入的方法跟 App 一樣:
    1. Hello.vue,他必須要埋入 <router-view>
    2. Kitty.vue
    3. HelloKitty.vue,他是 Hello 的子路由。
  5. 然後我們使用 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.vuebeforeUpdateupdate 是夾在 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.vuebeforeDestorydestroyed 是會發生於 Kitty.vuecreated, beforeMount 之後 ,且在 mounted 之前 ,這意味著什麼呢?

假設,你在 Kitty.vuecreated 有做了一些 全域事件 綁定,無論你是使用 EventBus 爾或者是綁定在 window 上面,舉例來說:

window.addEventListener('scroll', function () { ... }, false);

然後,假設你的 Hello.vuecreated 也有類似的作法,而你在 Hello.vuebeforeDestroy 也很乖的將這個事件解除,例如:

window.removeEventListener('scroll');

那麼,依照上述的邏輯,可怕的事情就發生了。你在 Kitty.vuecreated 所綁定的事件,由於 Hello.vuebeforeDestroyKitty.vuecreated 執行的關係,所以你所綁定的事件,就這樣被清除掉了。

綁一次不行,你可以綁兩次(欸不對)。

所以,你在解除綁定的時候,不建議使用上述的方式,無論是原生的 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 同一層,我們的點擊順序是:

  1. 先點擊 Hello
  2. 再點擊 Hello > Kitty
  3. 最後點擊 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
  • 以下是從 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

在這裡你會發現,比起從其他路徑進來的過程,如果都是在同一個父層級路由當中變換,那麼 Hello.vuemounted 就不會再被觸發,且,Hello.vuebeforeRouteUpdate 會在每次每個子路由離開( beforeRouteLeave )後,銷毀之前執行。

最後,我們按下外部連結,他連去了 Google,然後你會發現,所有的 Hooks 都不會運作。是的,連同什麼 beforeDestroy 還是 destroyed 都不會觸發。關於這一點請大家特別留意。


接著,我們來看一下「上一頁」這個動作會發生什麼事情?我們從剛剛的順序逆向操作:

  1. 我們從 Google 上一頁,回到 HelloKitty 頁面。
  2. 從 HelloKitty 上一頁,會回到 Kitty 頁面。
  3. 再從 Kitty 回到 Hello 頁面。
  4. 最後從 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();
  },
  // 後略...
}

順序之外的事

當然,順序很重要,就是因為這個順序很重要,所以你必須要留意許多可能會爆炸的地方:

  1. beforeCreate, beforeMount, created 順序 父元件 > 子元件
  2. mounted 順序 子元件 > 父元件
  3. 同父元件,以下事件只會做 1 次
    1. beforeCreate
    2. created
    3. beforeMount
    4. mounted
    5. beforeEnter
    6. beforeRouteEnter
  4. 同父元件,完全離開時(不同父元件)才會執行 beforeRouteLeave
  5. 在路由當中「重新整理頁面」的順序不太一樣,以 /hello/kitty 列舉如下:
    1. 先執行 Hello.vuebeforeEnter
    2. 接著子元件 HelloKitty.vuebeforeEnter
    3. 然後才是 App.vue 的系列動作:
      1. beforeCreate
      2. created
      3. beforeMount
      4. mounted
    4. 接著是 Hello.vuebeforeRouteEnter
    5. 接著子元件 HelloKitty.vuebeforeRouteEnter
    6. 再來是 App.vuebeforeUpdate
    7. 後續就是 Hello.vueHelloKitty.vue 與其他元件系列動作:
      1. beforeCreated
      2. created
      3. beforeMount
    8. 最後在依照路由順序 mounted,內部元件理論上會優先。
    9. 最後的最後 App.vueupdated
「重新整理畫面」的路由執行順序

看到這邊應該想放棄了吧?

放棄雖然可恥,但是有用!


小結

如果說,你覺得這邊已經是很複雜的地方了。那麼恭喜你,可以慢慢放棄沒有關係(欸)。因為在接下來的篇章裡面,我們會來聊聊比較特別的生命週期的狀況,那種你覺得他有 mounted 但是又沒有 mounted 的情況。

薛丁格的生命週期,敬請期待 (可以不要嗎)

ITHome 鐵人賽同步刊登 Router 與生命週期 Day 11