keep-alive 解决vue单页面应用tab页切换缓存问题

需求:

基于vue 2.6版本开发的功能复杂的单页面应用,产品提出的需求是像浏览器tab页同样,从菜单打开的若干个页面以前能够切换,且切换到其余页面时能保留其余页面正在编辑的数据。同时,关闭tab页以后,数据就不保留了。javascript

现状:

cn.vuejs.org/v2/api/#kee…vue

  • <keep-alive> 主要用于保留组件状态或避免从新渲染,包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
  • includeexclude 属性容许组件有条件地缓存。两者均可以用逗号分隔字符串、正则表达式或一个数组来表示:
  • 当组件在 <keep-alive> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。 在 2.2.0 及其更高版本中,activateddeactivated 将会在 <keep-alive> 树内的全部嵌套组件中触发。
  • <transition> 类似,<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出如今父组件链中。
  • 若是有多个条件性的子元素,<keep-alive> 要求同时只有一个子元素被渲染。

----------------> 为何解决不了问题?<------------------java

<keep-alive :include="keepaliveList">
	<router-view :key="$route.fullPath"/> </keep-alive> 复制代码

源码改动:

github.com/RobbinBaauw…node

// 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>, key: ?string, name: string): boolean {
  function matchesValue(value: string) {
    if (Array.isArray(pattern)) {
      return pattern.indexOf(value) > -1
    } else if (typeof pattern === 'string') {
      return pattern.split(',').indexOf(value) > -1
    } else if (isRegExp(pattern)) {
      return pattern.test(value)
    }
    /* istanbul ignore next */
    return false
  }
    // 相比于原来的代码主要差异是在这边对key 和name 都进行了匹配
  return (key && matchesValue(key)) || matchesValue(name);
}

function pruneCache (keepAliveInstance: any, filter: Function) {
    // keepAliveInstance:当前keep-alive组件
    // cache:全部缓存的key-组件vnode键值对
    // keys:全部缓存的key
    // _vnode:当前激活的组件vnode
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
        // 路由组件必需要有name
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(key, 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',
    // 抽象组件,还有如transition, slot等
  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指的是当前keep-alive组件
    this.$watch('include', val => {
      pruneCache(this, (key, name) => matches(val, key, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, (key, name) => !matches(val, key, name))
    })
  },

  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 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 { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, key, name))) ||
        // excluded
        (exclude && name && matches(exclude, key, name))
      ) {
          // include(包含的组件缓存生效) 与 exclude(排除的组件不缓存,优先级大于include)
        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. componnet name

匹配首先检查组件自身的 name 选项,若是 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值)。匿名组件不能被匹配。git

  1. computed/watch

在computed/watch中如果有引用到$route,那么要当心进行判空。不然会出现报错。

  1. activated & deactivated 能够在tab页激活或者失活的时候进行一些操做。好比激活的时候从新刷新列表。

目前已知的问题:

在作了tab页缓存以后,页面的Dom节点数量不少,多个页面并存,带来了必定的性能问题,还待解决。github

相关文章
相关标签/搜索