修改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 相关文章推荐
jquery.artwl.thickbox.js  一个非常简单好用的jQuery弹出层插件
Mar 01 Javascript
JS小功能(button选择颜色)简单实例
Nov 29 Javascript
jQuery mobile类库使用时加载导航历史的方法简介
Dec 04 Javascript
Javascript复制实例详解
Jan 28 Javascript
JavaScript鼠标特效大全
Sep 13 Javascript
Vue实现购物车功能
Apr 27 Javascript
javascript浏览器用户代理检测脚本实现方法
Oct 27 Javascript
红黑树的插入详解及Javascript实现方法示例
Mar 26 Javascript
vue中eslintrc.js配置最详细介绍
Dec 21 Javascript
angular中如何绑定iframe中src的方法
Feb 01 Javascript
微信小程序中weui用法解析
Oct 21 Javascript
Vue实现星级评价效果实例详解
Dec 30 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
php Undefined index和Undefined variable的解决方法
2008/03/27 PHP
php出现Cannot modify header information问题的解决方法大全
2008/04/09 PHP
php 获取百度的热词数据的代码
2012/02/18 PHP
采用memcache在web集群中实现session的同步会话
2014/07/05 PHP
PHP批量查询WordPress留言者E-mail地址实现方法
2015/02/15 PHP
php在apache环境下实现gzip配置方法
2015/04/02 PHP
实用的JS正则表达式(手机号码/IP正则/邮编正则/电话等)
2013/01/11 Javascript
推荐 21 款优秀的高性能 Node.js 开发框架
2014/08/18 Javascript
简介JavaScript中的unshift()方法的使用
2015/06/09 Javascript
jquery实现全选和全不选功能效果的实现代码【推荐】
2016/05/05 Javascript
jQuery模拟Marquee实现无缝滚动效果完整实例
2016/09/29 Javascript
ES6中Math对象的部分扩展
2017/02/20 Javascript
[42:00]完美世界DOTA2联赛PWL S3 Phoenix vs INK ICE 第一场 12.13
2020/12/17 DOTA
[01:36:17]DOTA2-DPC中国联赛 正赛 Ehome vs iG BO3 第一场 1月31日
2021/03/11 DOTA
python采集博客中上传的QQ截图文件
2014/07/18 Python
python添加模块搜索路径方法
2017/09/11 Python
Python pymongo模块常用操作分析
2018/09/01 Python
详解Python字典小结
2018/10/20 Python
numpy基础教程之np.linalg
2019/02/12 Python
基于Numpy.convolve使用Python实现滑动平均滤波的思路详解
2019/05/16 Python
python的几种矩阵相乘的公式详解
2019/07/10 Python
python定义类self用法实例解析
2020/01/22 Python
python上下文管理器异常问题解决方法
2021/02/07 Python
CSS3 新增选择器的实例
2019/11/13 HTML / CSS
详解通过HTML5 Canvas实现图片的平移及旋转变化的方法
2016/03/22 HTML / CSS
雅诗兰黛旗下专业男士保养领导品牌:Lab Series
2017/05/15 全球购物
JustFab加拿大:女鞋、靴子、手袋和服装在线
2018/05/18 全球购物
代码中finally中的代码会不会执行
2012/02/06 面试题
酒店副总岗位职责
2013/12/24 职场文书
重阳节登山活动方案
2014/02/03 职场文书
物流管理专业毕业生求职信
2014/03/23 职场文书
2014年平安建设工作总结
2014/11/19 职场文书
灵山大佛导游词
2015/02/04 职场文书
2015年超市收银员工作总结
2015/04/25 职场文书
校园文化艺术节开幕词
2016/03/04 职场文书
MySQL Innodb关键特性之插入缓冲(insert buffer)
2021/04/08 MySQL