Vue3核心源码解析 (五) : 内置组件2023-04-17 本文已影响0人
奋斗_登
<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>中缓存的组件数据进行删除和更新,其算法的核心实现如下:
LRU
上面的代码中,主要利用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对象,从而来处理缓存中的具体逻辑。
<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>中缓存的组件数据进行删除和更新,其算法的核心实现如下:
LRU
上面的代码中,主要利用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对象,从而来处理缓存中的具体逻辑。