<keep-alive>是Vue.js的一个内置组件,可以使被包罗的组件保存状态或制止重新渲染。下面来分析源码runtime-core/src/components/KeepAlive.ts的实现原理。
在setup方法中会创建一个缓存容器和缓存的key列表,其代码如下:
setup(props: KeepAliveProps, { slots }: SetupContext) {// keep-alive组件的上下文对象 const instance = getCurrentInstance()! // KeepAlive communicates with the instantiated renderer via the // ctx where the renderer passes in its internals, // and the KeepAlive instance exposes activate/deactivate implementations. // The whole point of this is to avoid importing KeepAlive directly in the // renderer to facilitate tree-shaking. const sharedContext = instance.ctx as KeepAliveContext // if the internal renderer is not registered, it indicates that this is server-side rendering, // for KeepAlive, we just need to render its children if (__SSR__ && !sharedContext.renderer) { return () => { const children = slots.default && slots.default() return children && children.length === 1 ? children[0] : children } }/* 缓存对象 */ const cache: Cache = new Map() const keys: Keys = new Set() // 替换内容 sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => { const instance = vnode.component! move(vnode, container, anchor, MoveType.ENTER, parentSuspense) // 处置惩罚props改变 patch( ... ) ... } // 替换内容 sharedContext.deactivate = (vnode: VNode) => { const instance = vnode.component! move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense) ... } } <keep-alive>本身实现了render方法,并没有使用Vue内置的render方法(颠末<template>内容提取、转换AST、render字符串等一系列过程),在实行<keep-alive>组件渲染时,就会实行这个render方法:
return () => { pendingCacheKey = null if (!slots.default) { return null }// 得到插槽中的第一个组件 const children = slots.default() const rawVNode = children[0] if (children.length > 1) { if (__DEV__) { warn(`KeepAlive should contain exactly one component child.`) } current = null return children } else if ( !isVNode(rawVNode) || (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) && !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE)) ) { current = null return rawVNode } let vnode = getInnerChild(rawVNode) const comp = vnode.type as ConcreteComponent // for async components, name check should be based in its loaded // inner component if available // 获取组件名称,优先获取组件的name字段 const name = getComponentName( isAsyncWrapper(vnode) ? (vnode.type as ComponentOptions).__asyncResolved || {} : comp ) // name不在include中大概exclude中,则直接返回vnode(没有存取缓存) const { include, exclude, max } = props if ( (include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name)) ) { current = vnode return rawVNode } const key = vnode.key == null ? comp : vnode.key const cachedVNode = cache.get(key) // clone vnode if it's reused because we are going to mutate it if (vnode.el) { vnode = cloneVNode(vnode) if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) { rawVNode.ssContent = vnode } } // #1513 it's possible for the returned vnode to be cloned due to attr // fallthrough or scopeId, so the vnode here may not be the final vnode // that is mounted. Instead of caching it directly, we store the pending // key and cache `instance.subTree` (the normalized vnode) in // beforeMount/beforeUpdate hooks. pendingCacheKey = key // 假如已经缓存了,则直接从缓存中获取组件实例给vnode,若还未缓存,则先辈行缓存 if (cachedVNode) { // copy over mounted state vnode.el = cachedVNode.el vnode.component = cachedVNode.component // 实行transition if (vnode.transition) { // recursively update transition hooks on subTree setTransitionHooks(vnode, vnode.transition!) }// 设置shapeFlag标志位,为了制止实行组件mounted方法 // avoid vnode being mounted as fresh vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE // make this key the freshest // 重新设置一下key包管最新 keys.delete(key) keys.add(key) } else { keys.add(key) // prune oldest entry // 当超出max值时,扫除缓存 if (max && keys.size > parseInt(max as string, 10)) { pruneCacheEntry(keys.values().next().value) } } // avoid vnode being unmounted vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE current = vnode return isSuspense(rawVNode.type) ? rawVNode : vnode } 在上面的代码中,当缓存的个数凌驾max(默认值为10)的值时,就会扫除旧的数据,这其中就包罗<keep-alive>的缓存更新计谋,其依照了LRU(Least Rencently Used)算法。
1. LRU算法
LRU算法根据数据的汗青访问记载来镌汰数据,其核心头脑是“假如数据最近被访问过,那么将来被访问的概率也更高”。使用这个思绪,我们可以对<keep-alive>中缓存的组件数据举行删除和更新,其算法的核心实现如下:
上面的代码中,主要使用map来存储缓存数据,使用map.keyIterator.next()来找到最久没有使用的key对应的数据,从而对缓存举行删除和更新。
2. 缓存VNode对象
在render方法中,<keep-alive>并不是直接缓存的DOM节点,而是Vue中内置的VNode对象,VNode颠末render方法后,会被替换成真正的DOM内容。起首通过slots.default().children[0]获取第一个子组件,获取该组件的name。接下来会将这个name通过include与exclude属性举行匹配,若匹配不乐成(阐明不必要举行缓存),则不举行任何使用直接返回VNode。必要注意的是,<keep-alive>只会处置惩罚它的第一个子组件,以是假如给<keep-alive>设置多个子组件,是无法见效的。
<keep-alive>尚有一个watch方法,用来监听include和exclude的改变,代码如下:
// prune cache on include/exclude prop change watch( () => [props.include, props.exclude], ([include, exclude]) => { // 监听include和exclude,在被修改时对cache举行修正 include && pruneCache(name => matches(include, name)) exclude && pruneCache(name => !matches(exclude, name)) }, // prune post-render after `current` has been updated { flush: 'post', deep: true } ) 这里的步伐逻辑是动态监听include和exclude的改变,从而动态地维护之前创建的缓存对象cache,着实就是对cache举行遍历,发现缓存的节点名称和新的规则没有匹配上时,就把这个缓存节点从缓存中摘除。下面来看pruneCache这个方法,代码如下:
function pruneCache(filter?: (name: string) => boolean) { cache.forEach((vnode, key) => { const name = getComponentName(vnode.type as ConcreteComponent) if (name && (!filter || !filter(name))) { pruneCacheEntry(key) } }) }遍历cache中的全部项,假如不符合filter指定的规则,则会实行pruneCacheEntry,代码如下:
function pruneCacheEntry(key: CacheKey) { const cached = cache.get(key) as VNode if (!current || !isSameVNodeType(cached, current)) { unmount(cached) } else if (current) { // current active instance should no longer be kept-alive. // we can't unmount it now but it might be later, so reset its flag now. resetShapeFlag(current) } // 烧毁VNode对应的组件实例 cache.delete(key) keys.delete(key) }上面的内容完成以后,当相应式触发时,<keep-alive>中的内容会改变,会调用<keep-alive>的render方法得到VNode,这里并没有用很深条理的diff去对比缓存前后的VNode,而是直接将旧节点置为null,用新节点举行替换,在patch方法中,直接掷中这里的逻辑,代码如下:
// n1为缓存前的节点,n2为将要替换的节点 if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) // 卸载旧节点 unmount(n1, parentComponent, parentSuspense, true) n1 = null }然后通过setup方法中的sharedContext.activate和sharedContext.deactivate来举行内容的替换,其核心是move方法,代码如下:
const move: MoveFn = () => { // 替换DOM ... hostInsert(el!, container, anchor) // insertBefore修改DOM }总结一下,<keep-alive>组件也是一个Vue组件,它通过自界说的render方法实现,而且使用了插槽。由于是直接使用VNode方式举行内容替换,不是直接存储DOM结构,因此不会实行组件内的生命周期方法,它通过include和exclude维护组件的cache对象,从而来处置惩罚缓存中的具体逻辑。 |