keep-alive是一个内置抽象组件(它自身不会渲染一个 DOM 元素,也不会出现在父组件链中,abstract: true
属性值决定)。包裹动态组件时,会缓存不活动的组件实例。而不是销毁它们。
其主要用于保留组件状态(保留在内存中),避免重新渲染
常用场景
- 从列表进入详情页面,再回到列表页面,不需要刷新列表页面
- 填写表单,进入下一步,返回上一步数据不会清空
- 长列表内容
使用语法
有三个属性Props:
- include 字符串或正则表达式。只有名称匹配的组件会被缓存。
- exclude 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
- max 最多可以缓存多少组件实例。超出上限使用LRU的策略置换缓存数据(选择最近最久未使用的页面予以淘汰)
// include="xxx,xx" xxx 是组件的名字
<keep-alive :include="whiteList" :exclude="blackList" :max="number">
// 1、动态组件中使用
<component :is="currentComponent"></component>
// 2、动态路由中使用
<router-view></router-view>
</keep-alive>
结合router 使用
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
export default new Router({
routes: [
{
path: '/xx',
name: 'Hello',
component: Hello,
meta: {
keepAlive: true // false不需要缓存, true 需要
}
}
]
tips: 有时需要特定情况下keep-alive,则需要利用router的钩子函数beforeRouteEnter
,beforeRouteLeave
进出页面,进行处理。
参考
keep-alive的生命周期
当引入keep-alive的时,钩子的触发顺序:created -> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。销毁逻辑不会执行。业务中可新增对应的生命周期函数使用。
keep-alive原理
源码地址 src/core/components/keep-alive.js
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判断当前组件虚拟dom是否渲染成真是dom的关键
props: {
include: patternTypes, // 缓存白名单
exclude: patternTypes, // 缓存黑名单
max: [String, Number] // 缓存的组件实例数量上限
},
created () {
this.cache = Object.create(null) // 缓存虚拟dom
this.keys = [] // 缓存的虚拟dom的健集合
},
destroyed () {
for (const key in this.cache) { // 删除所有的缓存
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
// 实时监听黑白名单的变动
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
// 先省略...
}
}
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 // 定义组件的缓存key
// 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) // 调整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])
}
步骤分析:
- 获取keep-alive对象包括的第一个子组件对象
- 根据白黑名单是否匹配返回本身的vnode
- 根据vnode的cid和tag生成的key,在缓存对象中是否有当前缓存,如果有则返回,并更新key在keys中的位置
- 如果当前缓存对象不存在缓存,就往cache添加这个的内容,并且根据LRU算法删除最近没有使用的实例
- 设置为第一个子组件对象的keep-alive为true
面试
- 1、keep-alive,它不会生成真正的DOM节点,这是怎么做到的? 在keep-alive中,设置了abstract: true,那Vue就会跳过该组件实例。 最后构建的组件树中就不会包含keep-alive组件
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// 找到第一个非abstract的父组件实例
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
-
2、说说keep-alive 场景=》属性 =》生命周期=》 源码
-
3、keep-alive包裹的组件是如何使用缓存的? 在patch阶段,会执行createComponent函数:
// src/core/vdom/patch.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)
insert(parentElm, vnode.elm, refElm) // 将缓存的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
在首次加载被包裹组件时,由keep-alive.js中的render函数可知,vnode.componentInstance
的值是undefined,keepAlive的值是true,因为keep-alive组件作为父组件,它的render函数会先于被包裹组件执行;那么就只执行到i(vnode, false),后面的逻辑不再执行;
再次访问被包裹组件时,vnode.componentInstance
的值就是已经缓存的组件实例,那么会执行insert(parentElm, vnode.elm, refElm)
逻辑,这样就直接把上一次的DOM插入到了父元素中。
-
4、为什么keep-alive包裹的组件钩子函数只执行一次? 被缓存的组件实例会为其设置
keepAlive = true
,而在初始化组件钩子函数中,进行了判断 详细查看: -
LRU 算法 利用set处理
* 1. 现判定是否存在 若存在则将其放置最新位置
* 2. 存入cache
* 3. 判定插入后是否超出容器体积 若超出则移除首位
// 最后一个权重最大
class LRUCache {
constructor(capacity) {
this.cache = new Map()
this.capacity = capacity
}
get(k) {
// 是否有
if (!this.cache.has(k)) return -1
const v = this.cache.get(k)
// make sure it is latest
this.cache.delete(k)
this.cache.set(k, v)
return v
}
put(k, v) {
// delete if if it exists
if (this.cache.has(k)) this.cache.delete(k)
// store it in cache
this.cache.set(k, v)
// make sure not to exceed the range after store it in cache
// 大于长度 删除
if (this.cache.size > this.capacity) {
const first = this.cache.keys().next().value
this.cache.delete(first)
}
}
}
利用哈希表+链表处理,实现操作是O(1).
伪代码:
- 初始化头尾两个节点
- moveToHead(node) 读取的时候将节点移到head
- addToHead(node) 新增节点时,将其移到最前面
- removeFromList 将节点从list上移出
- removeLRUItem 删除头节点 当level 长度够的时候
class ListNode {
constructor(key, val){
this.key = key
this.value = val
this.next = null
this.prev = null
}
}
class LRUCache {
constructor(level){
this.level = level
this.hash = {}
this.count = 0
this.vHead = new ListNode()
this.vTail = new ListNode()
this.vHead.next = this.vTail
this.vTail.prev = this.vHead
}
get(key) {
let node = this.hash[key]
if (node == null) return -1
this.moveToHead(node)
return node.value
}
put(key, value) {
let node = this.hash[key]
if (node == null) {
if (this.count == this.level) {
this.removeLRUItem()
}
let newNode = new ListNode(key, value)
this.hash[key] = newNode
this.addToHead(newNode)
this.count++
} else {
node.value = value
this.moveToHead(node)
}
}
moveToHead(node) {
this.removeFromList(node)
this.addToHead(node)
}
removeFromList(node) {
let temp1 = node.prev
let temp2 = node.next
temp1.next = temp2
temp2.prev = temp1
}
addToHead(node) {
node.prev = this.vHead
node.next = this.vHead.next
this.vHead.next.prev = node
this.vHead.next = node
}
removeLRUItem() {
let tail = this.popTail()
delete this.hash[tail.key]
this.count--
}
popTail() {
let tail = this.vTail.prev
this.removeFromList(tail)
return tail
}
}