一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情。
一、用法
<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。
<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
二、属性
include- 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude- 字符串或正则表达式。任何名称匹配的组件都不会被缓存。max- 数字。最多可以缓存多少组件实例。
三、何时用到keep-alive
打个比方,举个例子:我们在后台管理系统中有列表页,头部有查询条件,我们点击一条信息打开详情页,这时我们切换头部的tab又或者是返回希望查询条件还在,此时我们就可以使用keep-alive来做缓存。
四、源码分析
keep-alive核心源码主要在src/core/components/keep-alive.js中。
上图我们看到:
- 定义了
name为keep-alive。 abstract: true,抽象组件:不会渲染为 DOM 元素,也不会出现在组件的父组件链中。- props接受三个参数,参数的意思在上面说过。
- 然后就是几个生命周期函数,render方法几个主要部分。
created
created () {
this.cache = Object.create(null)
this.keys = []
},
- 定义了
cache对象,到时候存放需要缓存的DOM节点 keys来存放缓存的key
render
render () {
const slot = this.$slots.default
console.log('slot', slot)
const vnode: VNode = getFirstComponentChild(slot)
console.log('vnode', vnode)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
console.log('componentOptions', componentOptions)
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
console.log('name', name)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode // 不需要缓存的直接return vnode
}
const { cache, keys } = this
// 我们拿key,节点没有的话我们声明一个key
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]) {
// 如果有缓存,直接读取缓存中的componentInstance赋值
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
remove(keys, key) // 然后就是刷新key了,先移除,再push
keys.push(key)
} else {
// 没有缓存的话就缓存下做一个赋值操作,供后面缓存节点
// delay setting the cache until update
this.vnodeToCache = vnode
this.keyToCache = key
}
// 这里其实是个核心,将keepAlive设置为true,然后切换组件命中缓存之后,就会直接取缓存节点
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
render里面的东西还是有点的,我们先写个demo然后一边调试一边看:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app">
<button @click="handleClick('listCom')">列表</button>
<button @click="handleClick('detailCom')">详情</button>
<keep-alive>
<component :is="whileComponent"></component>
</keep-alive>
</div>
<script src="../../dist/vue.js"></script>
<script>
var listCom = {
template: `
<div>
<h1>列表组件</h1>
<input />
</div>
`
}
var detailCom = {
template: `
<div>
<h1>详情组件</h1>
<input />
</div>
`
}
var vm = new Vue({
el: '#app',
components: {
listCom, detailCom
},
data() {
return {
whileComponent: 'listCom'
}
},
methods: {
handleClick(componentId) {
this.whileComponent = componentId
},
},
})
</script>
</body>
</html>
例子比较简单把,点击列表按钮和详情按钮来切换组件,然后我们刷新页面:
- 首先通过插槽方式拿到
keep-alive里面的组件,然后拿到第一个子组件的节点,然后获取该子节点的componentOptions,我们看下这几个打印结果。
- 然后走到
if里面的逻辑通过getComponentName方法获取他的name
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
我们这时候是需要缓存的(不缓存的话直接返回vnode),我们根据节点的key来判断没有缓存过,如果有,则读取然后刷新keys;如果没有就添加缓存。
mounted
mounted () {
this.cacheVNode()
this.$watch('include', val => { // 监听白名单
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => { // 监听黑名单
pruneCache(this, name => !matches(val, name))
})
},
- 首先他有一个
cacheVNode方法
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
// 其实就是添加缓存
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
在render中,对于需要缓存的节点我们赋值了vnodeToCache, keyToCache,在这里就是把他们添加到缓存cache、keys里面去,然后去判断有没有超过最大缓存数量,超过的时候执行了pruneCacheEntry。
function pruneCacheEntry (
cache: CacheEntryMap,
key: string,
keys: Array<string>,
current?: VNode
) {
// 其实就是超过最大缓存数量的时候,将第一个缓存的节点移除
const entry: ?CacheEntry = cache[key]
if (entry && (!current || entry.tag !== current.tag)) {
entry.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
- 然后是监听
include、exclude,执行了pruneCache
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const entry: ?CacheEntry = cache[key]
if (entry) {
// 主要就是将监听的黑白名单过滤下,将不需要缓存的移除掉
const name: ?string = entry.name
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
updated
updated () {
// 组件重新渲染的时候,执行cacheVNode添加缓存
this.cacheVNode()
}
destroyed
destroyed () {
// 销毁组件的时候,循环去移除缓存
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}
注意它清除缓存不是简单的将cache、keys制空。
总结
keep-alive的核心源码我们阅读完了,相信大家一定有所收获;其实如果想真正理解它实现的原理,我们还需要配合vue渲染过程来阅读理解,今天我们先到这里~