前言
Vue 中有一个特别好用的组件 keep-alive 组件,我们在很多场景下,都是可以借助于这个组件来提升我们的产品体验,基本上0成本实现缓存效果。用的最多的场景就是和路由搭配,缓存不怎么更新的路由页面。
那这么好用的功能,背后是怎么实现的,有哪些可以学习的,一起来分析下。
正文分析
What
来自官网的介绍 cn.vuejs.org/v2/api/#kee…
可以看到,他的使用,更多的是跟随者动态组件一起使用。最核心的就是缓存组件实例,以提升性能。在官网上也有一个tab切换的示例,就是他的一种使用场景 cn.vuejs.org/v2/guide/co…
How
既然是一个内置组件,那么它肯定也就是一个按照组件来定义的,Vue 中核心的实现在 github.com/vuejs/vue/b… 这里
export default {
// 组件名字
name: 'keep-alive',
// 抽象组件 这是一个没有对外暴露的组件声明属性
// 作用的话,就是不渲染DOM 也不会出现在父组件的children中
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
methods: {
cacheVNode() {
const { cache, keys, vnodeToCache, keyToCache } = this
// 存在需要缓存的节点
if (vnodeToCache) {
const { tag, componentInstance, componentOptions } = vnodeToCache
// 缓存到 cache 对象中
cache[keyToCache] = {
name: getComponentName(componentOptions),
tag,
componentInstance,
}
keys.push(keyToCache)
// 判断是否超出了 max 最大缓存实例数
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
// 如果超出了 就销毁超出的
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
this.vnodeToCache = null
}
}
},
created () {
// 初始化缓存对象
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
// 销毁的时候 所有实例全部销毁
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.cacheVNode()
// 如果这些有更新 一样需要再次 check 一遍所有的缓存实例 是否应该缓存
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
updated () {
// 更新钩子 再次缓存
this.cacheVNode()
},
render () {
// 重点 render 的实现
// 可以获得组件内的默认内容 其实也就是 默认插槽内容
const slot = this.$slots.default
// 找到里边第一个组件节点
const vnode: VNode = getFirstComponentChild(slot)
// 通过 vnode 节点可以获得组件配置项
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
// 先删除掉这个 key 然后在push 保证这个 key 是新鲜的 超限check的时候 有用
// make current key freshest
remove(keys, key)
keys.push(key)
} else {
// delay setting the cache until update
// 设置好 vnodeToCache 当更新的时候 再去缓存 参考 updated 钩子中的逻辑
this.vnodeToCache = vnode
this.keyToCache = key
}
vnode.data.keepAlive = true
}
// 返回的就是内部的节点
return vnode || (slot && slot[0])
}
}
上边大概就是核心的一个流程:
- 默认进来,取得当前渲染的 vnode
- 然后进入 mounted 钩子,缓存上
- 当有更新的时候,再次调用 render,设置 vnodeToCache
- 到 updated 钩子,再次缓存上
- 下次如果命中缓存,直接用现有实例即可
你会发现要想很好的理解上述过程,要很好的理解 Vue 的生命周期,可以参考 cn.vuejs.org/v2/guide/in… 图如下:
此外,还有很多关于 vnode 上的属性,如:componentOptions、key、data、componentInstance、tag 等,以及相配合的在 Vue 中是如何识别和运用这些属性的:如何不创新新的实例,如何触发新的生命周期钩子 activated deactivated 等,如果你对所有的逻辑细节比较感兴趣,可以参考黄老师的 ustbhuangyi.github.io/vue-analysi…
Why
我们可以理解为 Vue 为什么提供了内置组件 keep-alive?
在前言的部分,我们也讲了在实际场景中,还是会遇到不少缓存组件的情况,在遇到路由场景的时候更甚。
那 Vue 的一个理念就是对开发者很友好,框架做了很多事情,使得开发者可以专注于自身的逻辑开发工作,这也是为什么全球会有那么多开发者钟爱它的原因之一。那从这个点出发,因为有这么多的需求,所以 Vue 也就提供了这么好用的内置组件也就不难理解了。
总结
我们可以看出,keep-alive 的组件实现并不复杂,全部文件也就 150 行上下,但是功能却很强大,所有的功能参考 cn.vuejs.org/v2/api/#kee… 。那么从这个组件上,我们可以学到些什么东西,有什么可以借鉴的吗?
Vue生命周期
组件定义虽然不多,但是却是用到了 Vue 中绝大多数的生命周期钩子,且是我们也能经常使用到的:created、mounted、updated、destroyed。这也是我们写好组件的基石,正确理解并运用他们,知道他们的关系和过程,以及在对应的生命周期中适合做什么样的事情。
额外提一点,在 created 生命周期钩子函数中,我们看到了如何给实例定义一些非响应式的对象,可以在 created 中直接 this.xxx = xxxx
的这种方式,而不是习惯性的把这些属性放到 data()
中去定义(会变为响应式对象,增加额外的开销),这种技巧值得我们学习和应用。
Vue手写render
在这里虽然十分简单,直接返回了默认插槽内容的节点,但是我们可以从这个点出发,在一些特殊场景,还是需要我们去手写 render 的,这部分也是需要我们去熟练运用的,详细的可以参考官网 cn.vuejs.org/v2/guide/re… 关于 render 函数的使用以及createElement、数据对象。
keys设计
keys 是一个数组,我们知道 keep-alive 还提供了一个能力:max
这个 prop,指定了最多可以缓存多少组件实例,一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。注意这里的关键:最久没有被访问的实例会被销毁掉。
这个是怎么做到的,排序吗?不用那么麻烦,通过上边的分析我们知道Vue中采用了一个很巧妙的做法:
remove(keys, key)
keys.push(key)
简单来说,将 keys 中现在对应的 key 删除掉,然后把这个 key 再 push 到数组最后即可。
通过这种方式,就可以保障了 keys 这个数组中最尾部的元素就是最新鲜的元素,最开始的元素就是最不新鲜的,在我们的场景中就可以对应为:最近被访问的实例。
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
可以看到实际代码也是这样的,当超出 max 的时候,就是销毁的keys[0]
对应的组件实例。
内存泄漏
在 destroyed 中,销毁了所有的实例,以释放在 cache 对象中缓存的对象们,这个就是对内存的管理,防止内存泄漏问题。这个是明面的内存销毁逻辑,大家只要注意了就不会遇到内存泄漏的问题了。
但是这个不是绝对的,我们看一下最新的这个PR github.com/vuejs/vue/p… ,我们发现解决了两个内存泄漏的 issue,直观看起来是不应该存在内存泄漏的才对。
这里就惊醒我们,要注意内存泄漏问题,他可能会是由于我们不经意之间写的代码所导致的。我们需要做到怎么利用开发者工具:
- 如何知道内存泄漏了
- 来定位&解决内存泄漏问题
其他小Tips
- 手工销毁组件实例,调用实例的
$destroy()
- 访问 $slots.default 获得默认子节点们
- vnode 上可以访问到 componentOptions以及 componentInstance,这俩还是很有用的
滴滴前端技术团队的团队号已经上线,我们也同步了一定的招聘信息,我们也会持续增加更多职位,有兴趣的同学可以一起聊聊。