Vue 中 keep-alive 組件與 router-view 組件的那點事

最近項目中有小伙伴找到我,問我“為啥他寫的頁面第一次進去可以觸發(fā) onCreate 函數(shù)拒秘,第二次再進的時候就不觸發(fā)了呢皇钞?”(因為我們項目是一個大型的項目嘿架,每個開發(fā)可能只接觸到自己開發(fā)的一小部分),然后我就說你可以試著在 activated 鉤子函數(shù)中做處理栅屏,然后他又接著問我“activated 鉤子函數(shù)又是怎么調(diào)用的呢飘千?”,ok栈雳!這小子是問上癮了护奈,我們下面就來詳細解析一下。

keep-alive

<keep-alive> 包裹動態(tài)組件時哥纫,會緩存不活動的組件實例霉旗,而不是銷毀它們。和 <transition> 相似磺箕,<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素奖慌,也不會出現(xiàn)在組件的父組件鏈中。

當組件在 <keep-alive> 內(nèi)被切換松靡,它的 activateddeactivated 這兩個生命周期鉤子函數(shù)將會被對應執(zhí)行简僧。

在 2.2.0 及其更高版本中,activateddeactivated 將會在 <keep-alive> 樹內(nèi)的所有嵌套組件中觸發(fā)雕欺。

主要用于保留組件狀態(tài)或避免重新渲染岛马。

為了更好的來解析 <keep-alive>,我們 copy 到一份源碼(vue@^2.6.10)屠列,vue/src/core/components/keep-alive.js

/* @flow */

import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'

type VNodeCache = { [key: string]: ?VNode };

function getComponentName (opts: ?VNodeComponentOptions): ?string {
  return opts && (opts.Ctor.options.name || opts.tag)
}

function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1
  } else if (typeof pattern === 'string') {
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

const patternTypes: Array<Function> = [String, RegExp, Array]

export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot) // 獲取第一個子節(jié)點
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions) // 獲取節(jié)點的名稱
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name)) // 不在范圍內(nèi)的節(jié)點將不會被緩存
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key // 獲取緩存的 key 值
      if (cache[key]) { // 如果有緩存就使用緩存
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else { // 沒有緩存就將當前節(jié)點加入到緩存
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) { // 如果緩存超過最大限制將不再緩存
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true // 標記該節(jié)點為 keepAlive 類型
    }
    return vnode || (slot && slot[0])
  }
}

其實從源碼我們可以看到啦逆,代碼并沒有多少,還是比較簡單的笛洛,下面我們用一下 keep-alive 組件夏志。

Props

從源碼中我們可以看到,<keep-alive> 組件有三個屬性:

  • include - 字符串或正則表達式苛让。只有名稱匹配的組件會被緩存沟蔑。
  • exclude - 字符串或正則表達式。任何名稱匹配的組件都不會被緩存狱杰。
  • max - 數(shù)字瘦材。最多可以緩存多少組件實例。

下面我們結(jié)合 Demo 來分析一下仿畸。

我們直接用 vue-cli 創(chuàng)建一個簡單的 vue 項目食棕,取名為 keep-alive-demo:

vue create keep-alive-demo

然后選一下 Router 后一路回車:

1-1.png

我們修改一下 App.vue 文件:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<style>
#app {
  text-align: center;
}
</style>

然后 views 目錄創(chuàng)建一個 A 組件當作 頁面 A

<template>
  <div class="about">
    <h1>我是 a 頁面</h1>
    <router-link to="/pageB">點我跳轉(zhuǎn)到 b 頁面</router-link>
  </div>
</template>
<script>
  import LifeRecycle from "../life-recycle";
  export default {
    name: "page-a",
    mixins:[LifeRecycle]
  }
</script>

A 頁面很簡單朗和,里面一個按鈕鏈接到了 B 頁面。為了更好的顯示每個組件的生命周期簿晓,我們?yōu)槊總€頁面添加了一個 mixin

export default {
    computed: {
        name(){
            return this.$options.name;
        }
    },
    created(){
        console.log("created--->"+this.name);
    },
    activated() {
        console.log("activated--->"+this.name);
    },
    deactivated() {
        console.log("deactivated--->"+this.name);
    },
    destroyed() {
        console.log("destoryed--->"+this.name);
    }
}

直接 copy 一份 A.vue 代碼創(chuàng)建一個 頁面 B

<template>
  <div class="about">
    <h1>我是 b 頁面</h1>
  </div>
</template>
<script>
  import LifeRecycle from "../life-recycle";
  export default {
    name: "page-b",
    mixins:[LifeRecycle]
  }
</script>

然后修改一下 views/Home.vue

<template>
    <div class="home">
        <h1>我是首頁</h1>
        <router-link to="/pageA">點我跳轉(zhuǎn)到 a 頁面</router-link>
    </div>
</template>
<script>
    import LifeRecycle from "../life-recycle";

    export default {
        name: 'home',
        mixins: [LifeRecycle]
    }
</script>

給一個按鈕直接鏈接到了 頁面 A眶拉。

最后我們修改一下 router.js

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: "history",
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
    },
    {
      path: "/pageA",
      name: "pageA",
      component: () => import(/* webpackChunkName: "about" */ './views/A.vue')
    },
    {
      path: "/pageB",
      name: "pageB",
      component: () => import(/* webpackChunkName: "about" */ './views/B.vue')
    }
  ]
})

代碼很簡單,我就不詳細解析了抢蚀,一個簡單的 SPA(單頁面應用) 就搭建完成了镀层,三個平級的頁面 homepageA皿曲、pageB唱逢。

我們試著運行一下項目:

npm run serve
1-2.gif

可以看到:

  1. 首頁打開 home 頁面

    created--->home
    

    直接觸發(fā)了 home 頁面的 created 方法。

  2. home 頁面 ---> pageA 頁面

created--->page-a
destoryed--->home

home 頁面觸發(fā)了 destoryed 直接銷毀了屋休,然后觸發(fā)了pageA 頁面的 created 方法坞古。

  1. pageA 頁面 ---> pageB 頁面
created--->page-b
destoryed--->page-a

pageA 頁面觸發(fā)了 destoryed 直接銷毀了,然后觸發(fā)了pageB 頁面的 created 方法劫樟。

  1. pageB 頁面返回

    created--->page-a
    destoryed--->page-b
    

    pageB 頁面觸發(fā)了 destoryed 直接銷毀了痪枫,然后觸發(fā)了pageA 頁面的 created 方法。

  2. pageA 頁面返回

    created--->home
    destoryed--->page-a
    

    pageA 頁面觸發(fā)了 destoryed 直接銷毀了叠艳,然后觸發(fā)了home 頁面的 created 方法奶陈。

    效果是沒問題的,但是作為一個 SPA 的項目附较,這種用戶體驗肯定是不友好的吃粒,試想一下,你現(xiàn)在在一個 app 的首頁瀏覽頁面拒课,然后滑呀滑呀徐勃,滑動了很長的頁面好不容易看到了一個自己感興趣的東西,然后點擊查看詳情離開了首頁早像,再回到首頁時候肯定是想停留在之前瀏覽器的地方僻肖,而不是說重新又打開一個新的首頁,又要滑半天卢鹦,這種體驗肯定是不好的臀脏,而且也有點浪費資源,所以下面我們用一下 <keep-alive> 把首頁緩存起來冀自。

    我們修改一下 App.vue 文件:

    <template>
      <div id="app">
        <keep-alive>
          <router-view/>
        </keep-alive>
      </div>
    </template>
    

    可以看到揉稚,我們添加了一個<keep-alive> 組件,然后再次之前的操作:

    1. 首頁打開 home 頁面

      created--->home
      activated--->home
      

      直接觸發(fā)了 home 頁面的 created 方法凡纳。

    2. home 頁面 ---> pageA 頁面

    created--->page-a
    deactivated--->home
    activated--->page-a
    

    home 頁面觸發(fā)了 deactivated 變成非活躍狀態(tài),然后觸發(fā)了pageA 頁面的 activated 方法帝蒿。

    1. pageA 頁面 ---> pageB 頁面
    created--->page-b
    deactivated--->page-a
    activated--->page-b
    

    pageA 頁面觸發(fā)了 deactivated 變成非活躍狀態(tài)荐糜,然后觸發(fā)了pageB 頁面的 activated 方法。

    1. pageB 頁面返回

      deactivated--->page-b
      activated--->page-a
      

      pageB 頁面觸發(fā)了 deactivated 變成非活躍狀態(tài),然后觸發(fā)了pageA 頁面的 activated 方法暴氏。

    2. pageA 頁面返回

      deactivated--->page-a
      activated--->home
      

細心的童鞋應該已經(jīng)發(fā)現(xiàn)區(qū)別了吧延塑?每個頁面的 destoryed 不觸發(fā)了,替換成了 deactivated答渔,然后第一次創(chuàng)建頁面的時候除了之前的 created 還多了一個 activated 方法关带。

是的!當我們加了<keep-alive> 組件后沼撕,所有頁面都被緩存起來了宋雏,但是我們只需要緩存的是 home 頁面,我們該怎么做呢务豺?

  1. 利用 include 屬性規(guī)定緩存的范圍

    我們修改一下 App.vue<keep-alive> 組件添加 include 屬性:

     <keep-alive :include="['home']">
          <router-view/>
      </keep-alive>
    

    include 可以是一個字符串數(shù)組磨总,也可以是一個正則表達式,匹配的就是組件的名字笼沥,比如這里的 home蚪燕,其實就是 home 組件的名稱:

    ...
    export default {
            name: 'home',
            mixins: [LifeRecycle]
        }
        ...
    
  2. 利用 exclude 屬性規(guī)定不緩存的范圍

    這個剛好跟 include 屬性相反,我們可以修改一下 App.vue<keep-alive> 組件添加 exclude 屬性:

    ..
    <keep-alive :exclude="/page-/">
          <router-view/>
        </keep-alive>
    ...
    

到這里我們思考一個問題奔浅,<keep-alive> 是會幫我們緩存組件馆纳,但是緩存的數(shù)量小倒還好,數(shù)量大了就有點得不償失了汹桦,所以 vue 考慮到這個情況了鲁驶,然后給<keep-alive> 添加了一個 max 屬性,比如我們只需要緩存一個頁面营勤,我們只需要設置 :max=1 即可:

..
<template>
  <div id="app">
    <keep-alive :max="1">
      <router-view/>
    </keep-alive>
  </div>
</template>
...

<keep-alive> 每次會緩存最新的那個頁面:

  1. 首頁打開 home 頁面

    created--->home
    activated--->home
    

    直接觸發(fā)了 home 頁面的 created 方法新創(chuàng)建了一個頁面灵嫌,然后調(diào)用了 activated 方法激活了當前頁面。

  2. home 頁面 ---> pageA 頁面

    created--->page-a
    deactivated--->home
    activated--->page-a
    

    home 頁面觸發(fā)了 deactivated 變成了非活躍狀態(tài)葛作,然后觸發(fā)了pageA 頁面的 created 方法新創(chuàng)建了一個頁面寿羞,然后調(diào)用了 activated 方法激活了當前頁面。

  3. pageA 頁面點擊返回

    created--->home
    deactivated--->page-a
    activated--->home
    

    pageA 頁面觸發(fā)了 deactivated 變成了非活躍狀態(tài)赂蠢,然后觸發(fā)了home 頁面的 created 方法新創(chuàng)建了一個頁面绪穆,然后調(diào)用了 activated 方法激活了當前頁面。

當緩存頁面的個數(shù)大于最大限制的時候虱岂,每次都移除數(shù)據(jù)的第 0 個位置的緩存玖院,源碼為:

// 如果緩存數(shù) > 最大緩存數(shù),移除緩存數(shù)組的第 0 位置數(shù)據(jù)
if (this.max && keys.length > parseInt(this.max)) { 
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
...
function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key] // 獲取需要移除的緩存頁面
  if (cached && (!current || cached.tag !== current.tag)) { // 如果當前頁面跟緩存的頁面不一致的時候
    // 觸發(fā)移除的緩存頁面的 destroy 方法
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

比如當 :max="2" 的時候第岖,home ---> pageA ---> pageB难菌,當進入 pageB 的時候,home 頁面就會被銷毀蔑滓,會觸發(fā) home 頁面的 destroyed 方法郊酒。

到這里 <keep-alive> 組件的基本用法我們算是 ok 了遇绞,我們解析來分析一下項目中會經(jīng)常遇到的一些問題。

activated 生命周期

通過上面的 demo 我們可以知道燎窘,當頁面被激活的時候會觸發(fā)當前頁面的 activated 方法摹闽,那么 vue 是在什么時候才會去觸發(fā)這個方法呢?

我們找到 vue 源碼位置 /vue/src/core/vdom/create-component.js:

...
insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // 在更新過程中褐健,一個緩存的頁面的子組件可能還會改變付鹿,
        // 當前的子組件并不一定就是最后的子組件,所以這個時候去調(diào)用 activaved 方法會不準確
        // 當頁面都組件更新完畢之后再去調(diào)用蚜迅。
        queueActivatedComponent(componentInstance)
      } else {
        // 遞歸激活所有子組件
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },
    ...
export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    // 先循環(huán)調(diào)用所有子組件的 activated 方法
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    // 再調(diào)用當前組件的 activated 方法
    callHook(vm, 'activated')
  }
}

當前 vnode 節(jié)點被插入的時候會判斷當前 vnode 節(jié)點 data 上是不是有 keepAlive 標記舵匾,有的話就會激活自身和自己所有的子組件,通過源碼我們還發(fā)現(xiàn)慢叨,當組件第一次創(chuàng)建的時候 activated 方法是在 mounted 方法之后執(zhí)行纽匙。

deactivated 生命周期

通過上面的 demo 我們可以知道,當頁面被隱藏的時候會觸發(fā)當前頁面的 deactivated 方法拍谐,那么 vue 是在什么時候才會去觸發(fā)這個方法呢烛缔?

activated 方法一樣,我們找到 vue 源碼位置 /vue/src/core/vdom/create-component.js:

...
destroy (vnode: MountedComponentVNode) {
    const { componentInstance } = vnode
    if (!componentInstance._isDestroyed) {
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
...
export function deactivateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    vm._inactive = true
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    callHook(vm, 'deactivated')
  }
}

當前vnode 節(jié)點被銷毀的時候轩拨,會判斷當前節(jié)點是不是有 keepAlive 標記践瓷,有的話就不會直接調(diào)用組件的 destroyed 了,而是直接調(diào)用組件的 deactivated 方法亡蓉。

那么節(jié)點的 keepAlive 是啥時候被標記的呢晕翠?還記得我們的 <keep-alive> 組件的源碼不?

vue/src/core/components/keep-alive.js:

...
render () {
    ...
      vnode.data.keepAlive = true // 標記該節(jié)點為 keepAlive 類型
    }
    return vnode || (slot && slot[0])
  }
...

ok砍濒!看到這里是不是就一目了然了呢淋肾?

router-view

router-view 組件的基本用法跟原理我就不在這里解析了,感興趣的童鞋可以去看 官網(wǎng) 爸邢,也可以去看我之前的一些文章 前端入門之(vue-router全解析二)樊卓。

我們看一下router-view 組件中的源碼:

import { warn } from '../util/warn'
import { extend } from '../util/misc'
import { handleRouteEntered } from '../util/route'

export default {
  name: 'RouterView',
  functional: true,
  props: {
    name: {
      type: String,
      default: 'default'
    }
  },
  render (_, { props, children, parent, data }) {
    // used by devtools to display a router-view badge
    data.routerView = true

    // directly use parent context's createElement() function
    // so that components rendered by router-view can resolve named slots
    const h = parent.$createElement
    const name = props.name
    const route = parent.$route
    const cache = parent._routerViewCache || (parent._routerViewCache = {})

    // determine current view depth, also check to see if the tree
    // has been toggled inactive but kept-alive.
    let depth = 0
    let inactive = false
    while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    // render previous view if the tree is inactive and kept-alive
    if (inactive) {
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        // #2301
        // pass props
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
        // render previous empty view
        return h()
      }
    }

    const matched = route.matched[depth]
    const component = matched && matched.components[name]

    // render empty node if no matched route or no config component
    if (!matched || !component) {
      cache[name] = null
      return h()
    }

    // cache component
    cache[name] = { component }

    // attach instance registration hook
    // this will be called in the instance's injected lifecycle hooks
    data.registerRouteInstance = (vm, val) => {
      // val could be undefined for unregistration
      const current = matched.instances[name]
      if (
        (val && current !== vm) ||
        (!val && current === vm)
      ) {
        matched.instances[name] = val
      }
    }

    // also register instance in prepatch hook
    // in case the same component instance is reused across different routes
    ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
      matched.instances[name] = vnode.componentInstance
    }

    // register instance in init hook
    // in case kept-alive component be actived when routes changed
    data.hook.init = (vnode) => {
      if (vnode.data.keepAlive &&
        vnode.componentInstance &&
        vnode.componentInstance !== matched.instances[name]
      ) {
        matched.instances[name] = vnode.componentInstance
      }

      // if the route transition has already been confirmed then we weren't
      // able to call the cbs during confirmation as the component was not
      // registered yet, so we call it here.
      handleRouteEntered(route)
    }

    const configProps = matched.props && matched.props[name]
    // save route and configProps in cache
    if (configProps) {
      extend(cache[name], {
        route,
        configProps
      })
      fillPropsinData(component, data, route, configProps)
    }

    return h(component, data, children)
  }
}

function fillPropsinData (component, data, route, configProps) {
  // resolve props
  let propsToPass = data.props = resolveProps(route, configProps)
  if (propsToPass) {
    // clone to prevent mutation
    propsToPass = data.props = extend({}, propsToPass)
    // pass non-declared props as attrs
    const attrs = data.attrs = data.attrs || {}
    for (const key in propsToPass) {
      if (!component.props || !(key in component.props)) {
        attrs[key] = propsToPass[key]
        delete propsToPass[key]
      }
    }
  }
}

function resolveProps (route, config) {
  switch (typeof config) {
    case 'undefined':
      return
    case 'object':
      return config
    case 'function':
      return config(route)
    case 'boolean':
      return config ? route.params : undefined
    default:
      if (process.env.NODE_ENV !== 'production') {
        warn(
          false,
          `props in "${route.path}" is a ${typeof config}, ` +
          `expecting an object, function or boolean.`
        )
      }
  }
}

很簡單,我就不一一解析了杠河,我們重點看一下 render 方法中的這一段代碼:

...
 render (_, { props, children, parent, data }) {
  ...
   while (parent && parent._routerRoot !== parent) {
      const vnodeData = parent.$vnode ? parent.$vnode.data : {}
      // 獲取當前頁面的層級數(shù)(嵌套路由情況)
      if (vnodeData.routerView) {
        depth++
      }
      if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
        inactive = true
      }
      parent = parent.$parent
    }
    data.routerViewDepth = depth

    // render previous view if the tree is inactive and kept-alive
    // 如果父組件為非激活狀態(tài)并且是被緩存的時候碌尔,就去渲染之前的組件
    if (inactive) {
      const cachedData = cache[name]
      const cachedComponent = cachedData && cachedData.component
      if (cachedComponent) {
        // #2301
        // pass props
        if (cachedData.configProps) {
          fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
        }
        return h(cachedComponent, data, children)
      } else {
        // render previous empty view
        return h()
      }
    }
 }
...

代碼現(xiàn)在可能不太好理解,我們還是利用 Demo 來實現(xiàn)一下這個場景券敌。

/**
/
+------------------+                  
| Home             |                  
| +--------------+ |                  
| | home1   home2  |              
| |              | |                 
| +--------------+ |                 
+------------------+
首頁下面有兩個嵌套路由 home1 跟 home2

/pageA
pageA 跟首頁平級 

/pageB
pageB 跟首頁平級 

*/

首先創(chuàng)建一個 Home1.vue 頁面:

<template>
  <div class="about">
    <h1>我是 home1</h1>
  </div>
</template>
<script>
  import LifeRecycle from "../life-recycle";
  export default {
    name: "home1",
    mixins:[LifeRecycle]
  }
</script>

然后創(chuàng)建一個 Home2.vue 頁面:

<template>
  <div class="about">
    <h1>我是 home2</h1>
  </div>
</template>
<script>
  import LifeRecycle from "../life-recycle";
  export default {
    name: "home2",
    mixins:[LifeRecycle]
  }
</script>

然后修改一下 Home 頁面唾戚,為其添加一個子路由:

<template>
    <div class="home">
        <h1>我是首頁</h1>
        <router-link to="/pageA">點我跳轉(zhuǎn)到 a 頁面</router-link>
        <div>
            <router-link to="/home1">點我切換到 home1 頁面</router-link>|
            <router-link to="/home2">點我跳轉(zhuǎn)到 home2 頁面</router-link>
            <router-view/>
        </div>
    </div>
</template>
<script>
    import LifeRecycle from "../life-recycle";

    export default {
        name: 'home',
        mixins: [LifeRecycle]
    }
</script>

然后修改一下 router.js 路由列表:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: "history",
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      children: [
        {
          path: 'home1',
          name: 'home1',
          component: () => import(/* webpackChunkName: "about" */ './views/Home1.vue'),
        },
        {
          path: 'home2',
          name: 'home2',
          component: () => import(/* webpackChunkName: "about" */ './views/Home2.vue'),
        }
      ]
    },
    {
      path: "/pageA",
      name: "pageA",
      component: () => import(/* webpackChunkName: "about" */ './views/A.vue')
    },
    {
      path: "/pageB",
      name: "pageB",
      component: () => import(/* webpackChunkName: "about" */ './views/B.vue')
    }
  ]
})

一切 ok 后,我們運行一下項目:

npm run serve
1-3.gif

可以看到:

  1. 打開 home1 頁面

     created--->home
     created--->home1
     activated--->home1
     activated--->home
    
  2. home1 --> home2

    created--->home2
    destoryed--->home1
    
  3. home2 --> pageA

    created--->page-a
    deactivated--->home2
    deactivated--->home
    activated--->page-a
    
  4. pageA 點擊返回

    deactivated--->page-a
    activated--->home2
    activated--->home
    

    此時 pageA 失活調(diào)用 deactivated 方法待诅,然后激活 home叹坦,而 home2home 的嵌套頁面,所以也直接走了 activated 方法被激活卑雁,而起作用的原因就是前面所列出的 router-view 的這一段代碼:

    ...
     render (_, { props, children, parent, data }) {
      ...
       while (parent && parent._routerRoot !== parent) {
          const vnodeData = parent.$vnode ? parent.$vnode.data : {}
          // 獲取當前頁面的層級數(shù)(嵌套路由情況)
          if (vnodeData.routerView) {
            depth++
          }
          if (vnodeData.keepAlive && parent._directInactive && parent._inactive) {
            inactive = true
          }
          parent = parent.$parent
        }
        data.routerViewDepth = depth
    
        // render previous view if the tree is inactive and kept-alive
         // 如果父組件為非激活狀態(tài)并且是被緩存的時候募书,就去渲染之前的組件
        if (inactive) {
          const cachedData = cache[name]
          const cachedComponent = cachedData && cachedData.component
          if (cachedComponent) {
            // #2301
            // pass props
            if (cachedData.configProps) {
              fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
            }
            return h(cachedComponent, data, children)
          } else {
            // render previous empty view
            return h()
          }
        }
     }
    ...
    

    router-view 會判斷當前組件的父組件是不是 keep-alive 的緩存組件轧钓,如果是的話,當前頁面(home2)離開的時候會把當前 home2 也緩存起來锐膜,下次再回到 home2 頁面的時候就不會再創(chuàng)建一個 home2 了,而是直接用緩存的組件弛房。

拓展

在 vue-router 的官網(wǎng)中我們看到這么一段介紹:

響應路由參數(shù)的變化

提醒一下道盏,當使用路由參數(shù)時,例如從 /user/foo 導航到 /user/bar文捶,原來的組件實例會被復用荷逞。因為兩個路由都渲染同個組件,比起銷毀再創(chuàng)建粹排,復用則顯得更加高效种远。不過,這也意味著組件的生命周期鉤子不會再被調(diào)用顽耳。

復用組件時坠敷,想對路由參數(shù)的變化作出響應的話,你可以簡單地 watch (監(jiān)測變化) $route 對象:

const User = {
  template: '...',
  watch: {
    $route(to, from) {
      // 對路由變化作出響應...
    }
  }
}

或者使用 2.2 中引入的 beforeRouteUpdate 導航守衛(wèi)

const User = {
  template: '...',
  beforeRouteUpdate (to, from, next) {
    // react to route changes...
    // don't forget to call next()
  }
}

兩個路由都渲染同個組件射富,比起銷毀再創(chuàng)建膝迎,復用則顯得更加高效。官網(wǎng)是這么介紹的胰耗,但是如果放棄這一點點優(yōu)化限次,我們硬是要重新創(chuàng)建一個組件怎么做呢?

比如我們現(xiàn)在的 Demo柴灯,我們修改一下代碼卖漫,讓 pageApageB 都共用一個 pageA 組件:

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'

Vue.use(Router)

export default new Router({
  mode: "history",
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home,
      children: [
        {
          path: 'home1',
          name: 'home1',
          component: () => import(/* webpackChunkName: "about" */ './views/Home1.vue'),
        },
        {
          path: 'home2',
          name: 'home2',
          component: () => import(/* webpackChunkName: "about" */ './views/Home2.vue'),
        }
      ]
    },
    {
      path: "/pageA",
      name: "pageA",
      component: () => import(/* webpackChunkName: "about" */ './views/A.vue')
    },
    {
      path: "/pageB",
      name: "pageB",
      component: () => import(/* webpackChunkName: "about" */ './views/A.vue')
    }
  ]
})

然后從 pageA 頁面跳轉(zhuǎn)到 pageB 頁面的時候,你會發(fā)現(xiàn)赠群,沒有任何反應羊始,頁面并不會被創(chuàng)建,而且繼續(xù)復用了 pageA乎串,ok店枣,接下來我們看一下 vue 源碼中判斷是否可以復用的規(guī)則是咋樣的。

我們找到 vue/src/core/vdom/patch.js 源碼中的這么一段代碼:

...
function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

所以 vue 源碼認為叹誉,只要 key 一致鸯两,并且標簽名等條件一直就認為是同一個組件了。

ok长豁!知道判斷條件后钧唐,我們來分析一下,我們并沒有給頁面組件指定 key 值匠襟,所以 undefined 是等于 undefined 的钝侠,然后 pageA 頁面跟 pageB 頁面都共用了一個 pageA 組件该园,所以 tag 名稱也是一樣的,然后后面的一些判斷條件也都成立帅韧,所以 vue 認為這兩個節(jié)點一致里初,也就不會再創(chuàng)建了,直接復用了忽舟,所以你會看到 pageA 點擊跳轉(zhuǎn)到 pageB 是沒有任何反應的双妨,那么知道原因后我們該怎么處理呢?

很簡單叮阅!直接給一個 key 就好了刁品,所以你會在很多 vue 項目中看到這樣的操作:

<template>
  <div id="app">
    <keep-alive>
      <!-- 設置當前 router-view 的 key 為 path,來解決復用問題 -->
      <router-view :key="$route.path"/>
    </keep-alive>
  </div>
</template>

看到這是不是就很能理解這個操作了呢浩姥?所以只有對源碼知根知底才能解決某些特殊的問題挑随,這也就是看源碼的重要性。

當然勒叠,頁面少的話拋開 vue 的復用是沒啥問題的兜挨,多頁面大項目的時候考慮內(nèi)存啥的還是保持官網(wǎng)說的 "比起銷毀再創(chuàng)建,復用則顯得更加高效" 較好眯分。

說到這里有小伙伴要疑問了暑劝,<keep-alive> 組件是怎么來獲取緩存的唯一 key 的呢?我們看一下它的做法:

const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key // 獲取緩存的 key 值
如果開發(fā)者有設置 key 值的話就直接用了颗搂,沒有的話用的是:
componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')

那么 componentOptions.Ctor.cid 又是啥呢担猛?Vue 中 cid 初始值是 0,每當調(diào)用了 Vue.extend() 方法后 cid 會自動加 1丢氢,然后賦值給當前的 vue 組件傅联。

總結(jié)

ok!寫了這么長的內(nèi)容疚察,雖然不管是解決 vue 組件的復用問題還是 activated 等方法的使用蒸走,網(wǎng)上一搜一大把都有對應的方案提供,但是我們得知道為什么要這么做呢貌嫡?所以這個時候就會強迫你去看源碼了比驻,我相信這一圈下來,你一定會有不一樣的收獲的岛抄。

加油吧别惦,騷年!

Demo 源碼

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夫椭,一起剝皮案震驚了整個濱河市掸掸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖扰付,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件堤撵,死亡現(xiàn)場離奇詭異,居然都是意外死亡羽莺,警方通過查閱死者的電腦和手機实昨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來盐固,“玉大人屠橄,你說我怎么就攤上這事∪虻玻” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵礁哄,是天一觀的道長长酗。 經(jīng)常有香客問我,道長桐绒,這世上最難降的妖魔是什么夺脾? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮茉继,結(jié)果婚禮上咧叭,老公的妹妹穿的比我還像新娘。我一直安慰自己烁竭,他們只是感情好菲茬,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著派撕,像睡著了一般婉弹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上终吼,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天镀赌,我揣著相機與錄音,去河邊找鬼际跪。 笑死商佛,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的姆打。 我是一名探鬼主播良姆,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼幔戏!你這毒婦竟也來了歇盼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤评抚,失蹤者是張志新(化名)和其女友劉穎豹缀,沒想到半個月后伯复,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡邢笙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年啸如,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片氮惯。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡叮雳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出妇汗,到底是詐尸還是另有隱情帘不,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布杨箭,位于F島的核電站寞焙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏互婿。R本人自食惡果不足惜捣郊,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望慈参。 院中可真熱鬧呛牲,春花似錦、人聲如沸驮配。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽壮锻。三九已至畜侦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間躯保,已是汗流浹背旋膳。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留途事,地道東北人验懊。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像尸变,于是被迫代替她去往敵國和親义图。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內(nèi)容