修改vue源码实现动态路由缓存的方法


Posted in Javascript onJanuary 21, 2020

动态路由

官网解读 :我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果。

即如果你有一个 盘点录入单 路由,但你想通过不同的传不同的 ID 来加载 CheckInputInfo 这个组件,若采用 params 方式,这时只需要在 path 后面配置 /:taskId 即可实现 CheckInputInfo/1 CheckInputInfo/2 这样的路由,同时可以通过 this.$route.params.taskId 来获取当前路由的 taskId

{
  path: 'CheckInputInfo/:taskId',
  meta: {
   title: '盘点录入单'
  },
  name: 'CheckInputInfo',
  component: () => import('@/view/Check/CheckInputInfo.vue')
 }

类似的,同样也可使用 query 方式,这时只需要在 path 后面配置 :taskId 即可实现 CheckInputInfo?taskId=1 CheckInputInfo?taskId=2 这样的路由,同时可以通过 this.$route.query.taskId 来获取当前路由的 taskId

{
  path: 'CheckInputInfo:taskId',
  meta: {
   title: '盘点录入单'
  },
  name: 'CheckInputInfo',
  component: () => import('@/view/Check/CheckInputInfo.vue')
 }

vue-router 通过配置 params query 来实现动态路由,并可通过 this.$route.xx 来获取当前的 params query ,省去了直接操作或处理 window.location ,还是挺方便的。

注意 :当使用路由参数时,例如从 /user/foo 导航到 /user/bar,原来的组件实例会被复用。因为两个路由都渲染同个组件,比起销毁再创建,复用则显得更加高效。不过,这也意味着组件的生命周期钩子不会再被调用。

解读:在不使用 keep-alive 的情况下,我们每次加载路由,这时会重新 render 当前路由挂载的 component ,但若这两个路由是同一个路由组件配置的动态路由, vue 为了性能设计了不会重新 render

这显然不符合我们的预期,那么该如何在动态路由下拥有完整的生命周期呢?答案是 keep-alive

keep-alive

官网解读 :keep-alive 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 transition 相似,keep-alive 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。在 2.2.0 及其更高版本中,activated 和 deactivated 将会在 树内的所有嵌套组件中触发。当组件在 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。

keep-alive通过缓存Vnode的方式解决了SPA最为关键的性能问题。以下,我就按步骤来分析以下:

一、路由触发路由组件重新render的问题

1、不缓存模式:

<router-view></router-view>

修改vue源码实现动态路由缓存的方法

每次切换都会重新 render ,执行整个生命周期,每次切换时,重新 render ,重新请求,,必然不满足需求。

2、缓存模式:

<keep-alive>
 <router-view></router-view>
</keep-alive>

修改vue源码实现动态路由缓存的方法

只是在进入当前路由的第一次 render ,来回切换不会重新执行生命周期,且能缓存 router-view 的数据。

二、router-view 数据缓存问题

keep-alive 采用 render 函数来创建 Vnode ,一下是 vue v2.5.10 keep-alive.js render()

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 的数据。

动态路由缓存问题及如何实现

一、bug表象

最开始其实是不知道这个 bug 的,也是通过现象反推,然后由源码解决这个问题的,那就先从现象说起:

动态路由缓存的的具体表现在:

由动态路由配置的路由只能缓存一份数据。 keep-alive 动态路由只有第一个会有完整的生命周期,之后的路由只会触发 actived deactivated 这两个钩子。 一旦更改动态路由的某个路由数据,期所有同路由下的动态路由数据都会同步更新。

我们的期望其实是在使用 keep-alive 的情况下,动态路由能有非动态的表现,即拥有 完整的生命周期各自的数据缓存

二、发掘问题关键

入手 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> = [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, 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])
 }
}

如何集成

因为放假赶车的关系,粗略说一下,有问题直接在底下评论:

一、修改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"
},

二、修改所有本地 import vue 为本地文件:

// import Vue from 'vue'
import Vue from '../vue-2.5.10/src/core/index'

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
优化 JavaScript 代码的方法小结
Jul 16 Javascript
jquery插件开发之实现jquery手风琴功能分享
Mar 10 Javascript
JavaScript匿名函数用法分析
Feb 13 Javascript
JS实现的Select三级下拉菜单代码
Aug 20 Javascript
基于jQuery全屏焦点图左右切换插件responsiveslides
Sep 07 Javascript
jQuery-1.9.1源码分析系列(十)事件系统之事件体系结构
Nov 19 Javascript
vue中改变选中当前项的显示隐藏或者状态的实现方法
Feb 08 Javascript
如何在vue中使用ts的示例代码
Feb 28 Javascript
Vue 实现双向绑定的四种方法
Mar 16 Javascript
发布Angular应用至生产环境的方法
Dec 10 Javascript
基于vue通用表单解决方案的思考与分析
Mar 16 Javascript
Vue实现boradcast和dispatch的示例
Nov 13 Javascript
微信小程序图片自适应实现解析
Jan 21 #Javascript
微信小程序button标签open-type属性原理解析
Jan 21 #Javascript
Vue实现兄弟组件间的联动效果
Jan 21 #Javascript
微信小程序如何通过用户授权获取手机号(getPhoneNumber)
Jan 21 #Javascript
微信小程序如何实现精确的日期时间选择器
Jan 21 #Javascript
微信小程序如何实现点击图片放大功能
Jan 21 #Javascript
微信小程序修改checkbox的样式代码实例
Jan 21 #Javascript
You might like
比特率,大家看看这个就不用收音机音质去比MP3音质了
2021/03/01 无线电
PHP下载远程文件到本地存储的方法
2015/03/24 PHP
PHP简单实现HTTP和HTTPS跨域共享session解决办法
2015/05/27 PHP
Docker 如何布置PHP开发环境
2016/06/21 PHP
PHP处理CSV表格文件的常用操作方法总结
2016/07/01 PHP
JavaScript 中的replace方法说明
2007/04/13 Javascript
点击按钮自动加关注的代码(sina微博/QQ空间/人人网/腾讯微博)
2014/01/02 Javascript
javascript中解析四则运算表达式的算法和示例
2014/08/11 Javascript
Javascript 中创建自定义对象的方法汇总
2014/12/04 Javascript
js给selected添加options的方法
2015/05/06 Javascript
移动适配的几种方案(三种方案)
2016/11/25 Javascript
boostrap模态框二次弹出清空原有内容的方法
2018/08/10 Javascript
微信小程序上传图片并等比列压缩到指定大小的实例代码
2019/10/24 Javascript
微信小程序录音实现功能并上传(使用node解析接收)
2020/02/26 Javascript
JS正则表达式验证密码强度
2020/03/18 Javascript
在Python的struct模块中进行数据格式转换的方法
2015/06/17 Python
Python抓取框架Scrapy爬虫入门:页面提取
2017/12/01 Python
Ubuntu下Python2与Python3的共存问题
2018/10/31 Python
连接pandas以及数组转pandas的方法
2019/06/28 Python
python selenium 查找隐藏元素 自动播放视频功能
2019/07/24 Python
Python自动生成代码 使用tkinter图形化操作并生成代码框架
2019/09/18 Python
python 字段拆分详解
2019/12/17 Python
使用python实现数组、链表、队列、栈的方法
2019/12/20 Python
关于TensorFlow新旧版本函数接口变化详解
2020/02/10 Python
Python的in,is和id函数代码实例
2020/04/18 Python
Python unittest单元测试openpyxl实现过程解析
2020/05/27 Python
巴西购物网站:Onofre Agora
2020/06/08 全球购物
大学四年规划书范文
2013/12/27 职场文书
共产党员公开承诺书
2014/03/25 职场文书
北京故宫导游词
2015/01/31 职场文书
2015年禁毒宣传活动总结
2015/03/25 职场文书
安全教育片观后感
2015/06/17 职场文书
2016初一新生军训心得体会
2016/01/11 职场文书
使用@Value值注入及配置文件组件扫描
2021/07/09 Java/Android
python全面解析接口返回数据
2022/02/12 Python
根德5570型九灯四波段立体声收音机是电子管收音机的楷模 ? 再论5570
2022/04/05 无线电