关于keep-alive,看这篇文章就够了!

用法回顾

通过keep-alive包裹的组件,可以帮助我们将一些不常变动或者需要缓存内容的组件保存在内存中,保留组件的状态,避免多次渲染,提高页面性能。

vue 复制代码
<!-- 父页面 -->
<template>
  <div>
    <button @click="switchName('Child1')">组件1</button>
    <button @click="switchName('Child2')">组件2</button>
    <component :is="componentName"></component>
  </div>
</template>

<script>
// 引入子组件
import Child1 from "./Child1.vue";
import Child2 from "./Child2.vue";
export default {
  components: {
    Child1,
    Child2,
  },
  data() {
    return {
        componentName:'Child1'
    };
  },
  methods:{
    switchName(componentName){
        this.componentName = componentName
    }
  }
};
</script>

<!-- 子页面1 -->
<template>
    <div>
        <input type="text">
    </div>
</template>

<!-- 子页面2 -->
<template>
    <div>
        <input type="text">
    </div>
</template>

上面代码实现的效果如下:

可以看出,我们在组件1和组件2中分别输入了内容,但是切换了组件,以前输入的内容就不存在了,说明切换组件把之前的组件销毁了,当我们需要保存之前的内容时,我们需要用keep-alive将组件包裹起来。

vue 复制代码
<keep-alive>
    <component :is="componentName"></component>
</keep-alive>

接收参数

keep-alive组件支持接收3个参数,分别是:

  • include 字符串、数组、或正则表达式,名称匹配的组件会被缓存
  • exclude 字符串、数组、或正则表达式,名称匹配的组件不会被缓存
  • max 数字,最大支持缓存页面个数

代码示例如下:

vue 复制代码
<!-- 字符串 -->
<keep-alive include="Child1,Child2">
  <component :is="componentName"></component>
</keep-alive>

<!-- 正则 -->
<keep-alive :include="/Child1|Child2/">
  <component :is="componentName"></component>
</keep-alive>

<!-- 数组 -->
<keep-alive :include="['Child1','Child2']">
  <component :is="componentName"></component>
</keep-alive>
vue 复制代码
<!-- 最大缓存组件数 -->
<keep-alive :max="5">
  <component :is="componentName"></component>
</keep-alive>

实现原理

js 复制代码
export default {
    name: 'keep-alive',
    abstract: true, //表示抽象组件,判断组件是否需要渲染成真是的dom 

    // 组件接收的参数
    props: {
        include: [String, RegExp, Array],
        exclude: [String, RegExp, Array],
        max: [String, Number]
    },

    // 初始化定义两个属性:cache、keys
    created() {
        this.cache = Object.create(null)
        this.keys = []
    },

    // 组件销毁时
    destroyed() {
        for (const key in this.cache) {
            pruneCacheEntry(this.cache, key, this.keys)
        }
    },

    // 观测incluede、exclude变化
    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 = getFirstComponentChild(slot)
        /* 获取该组件节点的componentOptions */
        const componentOptions = vnode && vnode.componentOptions

        if (componentOptions) {
            /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
            const name = getComponentName(componentOptions)

            const { include, exclude } = this
            /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
            if (
                (include && (!name || !matches(include, name))) ||
                // excluded
                (exclude && name && matches(exclude, name))
            ) {
                return vnode
            }

            // 以下走缓存
            const { cache, keys } = this
            // 获取组件的key值
            const key = vnode.key == null
                ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
                : vnode.key
            //如果命中缓存 
            if (cache[key]) {
                // 直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个
                vnode.componentInstance = cache[key].componentInstance
                remove(keys, key)
                keys.push(key)
            } else {
                //没有命中缓存,将一下组件缓存
                cache[key] = vnode
                keys.push(key)
                // 如果长度超过了max 则删除第一个
                if (this.max && keys.length > parseInt(this.max)) {
                    pruneCacheEntry(cache, keys[0], keys, this._vnode)
                }
            }
            //设置keepAlive标记位,缓存组件的钩子函数要用到
            vnode.data.keepAlive = true
        }
        return vnode || (slot && slot[0])
    }
}

我们来分析下以上源码:

created

在created中,定义了cachekeyscache是一个对象,用来储存需要缓存组件的信息,格式如下:

js 复制代码
this.cache = {
  'key1':'组件1',
  'key2':'组件2'
}

this.keys是一个数组,储存需要缓存组件的key,即this.cache对应的键。

destroyed

<keep-alive>组件销毁时,会调用destory,遍历this.cache对象,将组件销毁,并将销毁的组件从this.cache中删除:

js 复制代码
destroyed () {
    for (const key in this.cache) {
        pruneCacheEntry(this.cache, key, this.keys)
    }
}

//组件销毁函数
function pruneCacheEntry (cache,key,keys,current) {
  const cached = cache[key]
  //判断当前没有处于被渲染状态的组件,将其销毁
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

mounted

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

监听include、exclude的变化,如果变化了,表示缓存规则发生了改变,就调用pruneCache函数:

js 复制代码
function pruneCache (keepAliveInstance, filter) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode = cache[key]
    if (cachedNode) {
      const name = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

//组件销毁函数
function pruneCacheEntry (cache,key,keys,current) {
  const cached = cache[key]
  //判断当前没有处于被渲染状态的组件,将其销毁
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

pruneCache函数对this.cache遍历,取出每一项的name,用这个name与新的缓存规则进行匹配,如果匹配不上,表示新的缓存规则下,该组件已经不需要缓存,需要将组件实例销毁掉,并将其在thsi.cache中删除,即调用pruneCacheEntry函数。

render

js 复制代码
render() {
    /* 获取默认插槽中的第一个组件节点 */
    const slot = this.$slots.default
    const vnode = getFirstComponentChild(slot)
    /* 获取该组件节点的componentOptions */
    const componentOptions = vnode && vnode.componentOptions

    if (componentOptions) {
        /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */
        const name = getComponentName(componentOptions)

        const { include, exclude } = this
        /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */
        if (
            (include && (!name || !matches(include, name))) ||
            // excluded
            (exclude && name && matches(exclude, name))
        ) {
            return vnode
        }

        // 以下走缓存
        const { cache, keys } = this
        // 获取组件的key值
        const key = vnode.key == null
            ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
            : vnode.key
        //如果命中缓存 
        if (cache[key]) {
            // 直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个
            vnode.componentInstance = cache[key].componentInstance
            remove(keys, key)
            keys.push(key)
        } else {
            //没有命中缓存,将组件缓存
            cache[key] = vnode
            keys.push(key)
            // 如果长度超过了max 则删除第一个
            if (this.max && keys.length > parseInt(this.max)) {
                pruneCacheEntry(cache, keys[0], keys, this._vnode)
            }
        }
        //设置keepAlive标记位,缓存组件的钩子函数要用到
        vnode.data.keepAlive = true
    }
    //将vnode返回
    return vnode || (slot && slot[0])
}

render函数主要做了几件事:

  • 1.获取第一个组件和组件名(keep-alive只处理第一个子元素)
  • 2.组件name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode
  • 3.生成组件key,如果keycache中存在,说明已经缓存过该组件,直接从缓存中拿 vnode 的组件实例,此时重新调整该组件key的顺序,将其从原来的地方删掉并重新放在this.keys中最后一个。
  • 4.如果不在,表明该组件还没有被缓存过,将该组件的key为键,组件vnode为值,将其存入this.cache中,并且把key存入this.keys中。此时再判断this.keys中缓存组件的数量是否超过了最大缓存数量值this.max,如果超过了,则把第一个缓存组件删掉。

包裹的组件渲染

js 复制代码
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      i(vnode, false /* hydrating */)
    }

    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      // 将缓存的DOM(vnode.elm)插入父元素中
      insert(parentElm, vnode.elm, refElm) 
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

初始时,在keep-aliverender函数中,将包裹的组件添加到chche中,此时,vnode.componentInstance的值为undefined,未执行if (isDef(vnode.componentInstance)),再次访问包裹组件时,vnode.componentInstance的值就是已经缓存的组件实例,执行insert(parentElm, vnode.elm, refElm) ,将vnode.elm插入到父元素中,从而显示在页面。

即对应

js 复制代码
//如果命中缓存 
if (cache[key]) {
    //再次激活时获取vnode.componentInstance
    vnode.componentInstance = cache[key].componentInstance
    ...
} else {
    //初始化时先缓存
    cache[key] = vnode
    keys.push(key)
    ...
}
相关推荐
神夜大侠3 分钟前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱6 分钟前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号39 分钟前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy729341 分钟前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html
前端郭德纲1 小时前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
究极无敌暴龙战神X1 小时前
前端学习之ES6+
开发语言·javascript·ecmascript
王解1 小时前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里1 小时前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱1 小时前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
PyAIGCMaster1 小时前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python