修改vue源碼實現動態路由緩存 動態路由

修改vue源碼實現動態路由緩存javascript

動態路由

官網解讀:咱們常常須要把某種模式匹配到的全部路由,全都映射到同個組件。例如,咱們有一個 User 組件,對於全部 ID 各不相同的用戶,都要使用這個組件來渲染。那麼,咱們能夠在 vue-router 的路由路徑中使用「動態路徑參數」(dynamic segment) 來達到這個效果。

即若是你有一個盤點錄入單路由,但你想經過不一樣的傳不一樣的ID來加載CheckInputInfo這個組件,若採用params方式,這時只須要在path後面配置/:taskId便可實現CheckInputInfo/1CheckInputInfo/2這樣的路由,同時能夠經過this.$route.params.taskId來獲取當前路由的taskIdphp

{
    path: 'CheckInputInfo/:taskId',
    meta: {
      title: '盤點錄入單'
    },
    name: 'CheckInputInfo',
    component: () => import('@/view/Check/CheckInputInfo.vue')
  }
複製代碼

相似的,一樣也可以使用query方式,這時只須要在path後面配置:taskId便可實現CheckInputInfo?taskId=1CheckInputInfo?taskId=2這樣的路由,同時能夠經過this.$route.query.taskId來獲取當前路由的taskIdhtml

{
    path: 'CheckInputInfo:taskId',
    meta: {
      title: '盤點錄入單'
    },
    name: 'CheckInputInfo',
    component: () => import('@/view/Check/CheckInputInfo.vue')
  }
複製代碼

vue-router經過配置paramsquery來實現動態路由,並可經過this.$route.xx來獲取當前的paramsquery,省去了直接操做或處理window.location,仍是挺方便的。vue

注意:當使用路由參數時,例如從 /user/foo 導航到 /user/bar,原來的組件實例會被複用。由於兩個路由都渲染同個組件,比起銷燬再建立,複用則顯得更加高效。不過,這也意味着組件的生命週期鉤子不會再被調用。

解讀:在不使用keep-alive的狀況下,咱們每次加載路由,這時會從新render當前路由掛載的component,但若這兩個路由是同一個路由組件配置的動態路由,vue爲了性能設計了不會從新renderjava

這顯然不符合咱們的預期,那麼該如何在動態路由下擁有完整的生命週期呢?答案是keep-alivenode

keep-alive

官網解讀:keep-alive 包裹動態組件時,會緩存不活動的組件實例,而不是銷燬它們。和 transition 類似,keep-alive 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出如今組件的父組件鏈中。在 2.2.0 及其更高版本中,activated 和 deactivated 將會在 樹內的全部嵌套組件中觸發。當組件在 內被切換,它的 activated 和 deactivated 這兩個生命週期鉤子函數將會被對應執行。

keep-alive經過緩存Vnode的方式解決了SPA最爲關鍵的性能問題。如下,我就按步驟來分析如下:ios

1、路由觸發路由組件從新render的問題

一、不緩存模式:
<router-view></router-view>
複製代碼

每次切換都會從新render,執行整個生命週期,每次切換時,從新render,從新請求,,必然不知足需求。git

二、緩存模式:
<keep-alive>
  <router-view></router-view>
</keep-alive>
複製代碼

只是在進入當前路由的第一次render,來回切換不會從新執行生命週期,且能緩存router-view的數據。github

2、router-view 數據緩存問題

keep-alive採用render函數來建立Vnode,一下是vue v2.5.10keep-alive.jsrender()web

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        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
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        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
    }
    return vnode || (slot && slot[0])
  }
}
複製代碼

render是獲取到Vnode,若cache[key]存在,則:

vnode.componentInstance = cache[key].componentInstance
複製代碼

不然,將Vnode保存在cache裏:

cache[key] = vnode
複製代碼

因而當時用keep-alive時,咱們就能夠保存每一個route-view的數據。

動態路由緩存問題及如何實現

1、bug表象

最開始實際上是不知道這個bug的,也是經過現象反推,而後由源碼解決這個問題的,那就先從現象提及:

動態路由緩存的的具體表如今:

  • 由動態路由配置的路由只能緩存一份數據。
  • keep-alive動態路由只有第一個會有完整的生命週期,以後的路由只會觸發 activeddeactivated這兩個鉤子。
  • 一旦更改動態路由的某個路由數據,期全部同路由下的動態路由數據都會同步更新。

咱們的指望實際上是在使用keep-alive的狀況下,動態路由能有非動態的表現,即擁有完整的生命週期各自的數據緩存

2、發掘問題關鍵

入手keep-alive源碼發現,其實問題就出如今這一步:

if (
  // not included
  (include && (!name || !matches(include, name))) ||
  // excluded
  (exclude && name && matches(exclude, name))
) {
  return vnode
}
複製代碼

經過上面的表象其實能夠探究出,router-view實際上是已經緩存了,並且一個動態路由的router-view都是經過了if判斷返回了Vnode。那麼再看一下這個name是什麼:

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

const name: ?string = getComponentName(componentOptions)
複製代碼

這裏的opts其實對應的就是VueComponent$options,而this.$options.name不就是對應着得.vue文件裏聲明的name屬性。而後又想到,怪不得配置路由的時候要求提供的name屬性要和組件內部的name值保持一致。

看到這裏,問題已經水落石出了,由於動態路由配置的組件相同,getComponentName每次返回相同name,而後render()去緩存了相同的Vnode,且只能緩存了一份。既然如此,只要能正確的緩存Vnode和取出Vnode,動態路由狀況下,keep-alive依然能正常運行。

修改Vue源碼

上面說到了是由於動態路由組件名的問題,若是將緩存的key設置爲惟一不就好了嗎?

因而在router-view上配置key,key取得師path,永遠惟一:

<keep-alive :include="cacheList">
  <router-view :key="$route.path"></router-view>
</keep-alive>
複製代碼

而後修改keep-alive.js源碼,以下(由於放假的關係不詳細說了,直接貼源碼,實現的人就是我,也是第一個,github上此BUG目前仍是open狀態):

/* 
*@flow
*modify by LK 20190624
*/


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>, key: string | Number): boolean {
  if (Array.isArray(pattern)) {
    return pattern.indexOf(key) > -1
  } else if (typeof pattern === 'string') {
    return pattern.split(',').indexOf(key) > -1
  } else if (isRegExp(pattern)) {
    return pattern.test(key)
  }
  /* 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 (key && !filter(key)) {
        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> = [StringRegExpArray]

export default {
  name'keep-alive',
  abstracttrue,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [StringNumber]
  },

  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, key => matches(val, key))
    })
    this.$watch('exclude', val => {
      pruneCache(this, key => !matches(val, key))
    })
  },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    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
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!key || !matches(include, key))) ||
        // excluded
        (exclude && key && matches(exclude, key))
      ) {
        return vnode
      }

      const { cache, keys } = this

      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        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
    }
    return vnode || (slot && slot[0])
  }
}
複製代碼

如何集成

由於放假趕車的關係,粗略說一下,有問題直接在底下評論:

1、修改package.json:

npm install 時不下載vue,修改packjson.js改成本地的vue:"vue": "file:./vue2.5.0/"

"dependencies": {
  "axios""^0.18.0",
  "clipboard""^2.0.0",
  "codemirror""^5.38.0",
  "countup""^1.8.2",
  "cropperjs""^1.2.2",
  "dayjs""^1.7.7",
  "echarts""^4.0.4",
  "html2canvas""^1.0.0-alpha.12",
  "iview""^3.2.2",
  "iview-area""^1.5.17",
  "js-cookie""^2.2.0",
  "simplemde""^1.11.2",
  "sortablejs""^1.7.0",
  "tree-table-vue""^1.1.0",
  "v-org-tree""^1.0.6",
  "vue""file:./vue2.5.0/",
  "vue-i18n""^7.8.0",
  "vue-router""^3.0.1",
  "vuedraggable""^2.16.0",
  "vuex""^3.0.1",
  "wangeditor""^3.1.1",
  "xlsx""^0.13.3"
},
複製代碼

2、修改全部本地import vue 爲本地文件:

// import Vue from 'vue'
import Vue from '../vue-2.5.10/src/core/index'
複製代碼
相關文章
相關標籤/搜索