乐乐不殆 發表於 2026-1-11 13:10:00

彻底弄懂KeepAlive

<h1 data-id="heading-0">🧑‍💻 写在开头</h1>
<p>点赞 + 收藏 === 学会🤣🤣🤣</p>
<div>
<h2 data-id="heading-0">前言</h2>
<p>开发过Vue应用的同学对<code>KeepAlive</code>功能应该都不陌生了,但是大家对它的理解是只停留在知道怎么用的阶段 还是说清晰的知道它内部的实现细节呢,在项目中因<code>KeepAlive</code>导致的的Bug能第一时间分析出来原因并且找到解决方法呢。这篇文章的目的就是想结合Vue渲染的核心细节来重新认识一下<code>KeepAlive</code>这个功能。</p>
<p>文章是基于<strong>Vue3.5.24</strong>版本做的分析</p>
<p>接下来我将通过对几个问题的解释,来慢慢梳理<code>KeepAlive</code>的细节。</p>
<h2 data-id="heading-1">带着问题弄懂KeepAlive</h2>
<h3 data-id="heading-2">1.编写的Vue文件在浏览器运行时是什么样子的?</h3>
<p>看一个下面的简单例子</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">&lt;template&gt;
&lt;div&gt;{{ count }}&lt;/div&gt;
&lt;/template&gt;

&lt;script lang="ts" setup&gt;
import { ref } from 'vue'

const count = ref(0)
&lt;/script&gt;</pre>
</div>
</div>
<p>我们写的这么简单的一段代码,在运行前会被编译成下面这个样子,传送门Vue SFC Playground</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">import { defineComponent as _defineComponent } from 'vue'
import { ref } from 'vue'


const __sfc__ = /*@__PURE__*/_defineComponent({
__name: 'App',
setup(__props, { expose: __expose }) {
__expose();

const count = ref(0)

const __returned__ = { count }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}

});
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, _toDisplayString($setup.count), 1 /* TEXT */))
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__</pre>
</div>
<div>
<div>
<p>通过结果可以看到,Vue文件中的内容编译后变成了一个普通的JS对象。<br>
其中主要包含以下几个属性<br>
_name: 组件的名称,未明确定义组件名称的情况会使用文件的名称作为组件的名称。<br>
setup: 组件中定义的setup函数,默认返回了定义的响应式数据。<br>
render: 渲染函数,通过将template模板编译而来,返回值是一个VNode。<br>
_file: 组件的源文件名称。</p>
<h3 data-id="heading-3">2.组件需要满足什么条件才会被缓存,缓存的是什么?</h3>
<p>想要回答好这个问题,就需要结合<code>KeepAlive</code>组件的源码。</p>
</div>

</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,

__isKeepAlive: true,

props: {
    include: ,
    exclude: ,
    max: ,
},

setup(props: KeepAliveProps, { slots }: SetupContext) {
    const instance = getCurrentInstance()!

    const sharedContext = instance.ctx as KeepAliveContext

    if (__SSR__ &amp;&amp; !sharedContext.renderer) {
      return () =&gt; {
      const children = slots.default &amp;&amp; slots.default()
      return children &amp;&amp; children.length === 1 ? children : children
      }
    }

    // key -&gt; vNodeMap结构
    const cache: Cache = new Map()
    // 所有缓存的Key,保证当缓存数量超过max指定的值后,准确的移除最早缓存的实例
    const keys: Keys = new Set()
    // 当前渲染的vNode
    let current: VNode | null = null

    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      ;(instance as any).__v_cache = cache
    }

    const parentSuspense = instance.suspense

    const {
      renderer: {
      p: patch,
      m: move,
      um: _unmount,
      o: { createElement },
      },
    } = sharedContext
    const storageContainer = createElement('div')

    sharedContext.activate = (
      vnode,
      container,
      anchor,
      namespace,
      optimized,
    ) =&gt; {
      const instance = vnode.component!
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // in case props have changed
      patch(
      instance.vnode,
      vnode,
      container,
      anchor,
      instance,
      parentSuspense,
      namespace,
      vnode.slotScopeIds,
      optimized,
      )
      queuePostRenderEffect(() =&gt; {
      instance.isDeactivated = false
      if (instance.a) {
          invokeArrayFns(instance.a)
      }
      const vnodeHook = vnode.props &amp;&amp; vnode.props.onVnodeMounted
      if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
      }
      }, parentSuspense)

      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      // Update components tree
      devtoolsComponentAdded(instance)
      }
    }

    sharedContext.deactivate = (vnode: VNode) =&gt; {
      const instance = vnode.component!
      invalidateMount(instance.m)
      invalidateMount(instance.a)

      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      queuePostRenderEffect(() =&gt; {
      if (instance.da) {
          invokeArrayFns(instance.da)
      }
      const vnodeHook = vnode.props &amp;&amp; vnode.props.onVnodeUnmounted
      if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
      }
      instance.isDeactivated = true
      }, parentSuspense)

      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      // Update components tree
      devtoolsComponentAdded(instance)
      }

      // for e2e test
      if (__DEV__ &amp;&amp; __BROWSER__) {
      ;(instance as any).__keepAliveStorageContainer = storageContainer
      }
    }

    function unmount(vnode: VNode) {
      // reset the shapeFlag so it can be properly unmounted
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense, true)
    }

    function pruneCache(filter: (name: string) =&gt; boolean) {
      cache.forEach((vnode, key) =&gt; {
      const name = getComponentName(vnode.type as ConcreteComponent)
      if (name &amp;&amp; !filter(name)) {
          pruneCacheEntry(key)
      }
      })
    }

    // 根据key移除缓存的实例
    function pruneCacheEntry(key: CacheKey) {
      const cached = cache.get(key) as VNode
      if (cached &amp;&amp; (!current || !isSameVNodeType(cached, current))) {
      unmount(cached)
      } else if (current) {
      resetShapeFlag(current)
      }
      cache.delete(key)
      keys.delete(key)
    }

    // prune cache on include/exclude prop change
    watch(
      () =&gt; ,
      () =&gt; {
      include &amp;&amp; pruneCache(name =&gt; matches(include, name))
      exclude &amp;&amp; pruneCache(name =&gt; !matches(exclude, name))
      },
      // prune post-render after `current` has been updated
      { flush: 'post', deep: true },
    )

    // 渲染结束后 缓存当前实例
    let pendingCacheKey: CacheKey | null = null
    const cacheSubtree = () =&gt; {
      if (pendingCacheKey != null) {
      if (isSuspense(instance.subTree.type)) {
          queuePostRenderEffect(() =&gt; {
            cache.set(pendingCacheKey!, getInnerChild(instance.subTree))
          }, instance.subTree.suspense)
      } else {
          cache.set(pendingCacheKey, getInnerChild(instance.subTree))
      }
      }
    }
   
    // 在这Mounted和Updated钩子里面 缓存当前渲染的实例
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

    onBeforeUnmount(() =&gt; {
      cache.forEach(cached =&gt; {
      const { subTree, suspense } = instance
      const vnode = getInnerChild(subTree)
      if (cached.type === vnode.type &amp;&amp; cached.key === vnode.key) {
          resetShapeFlag(vnode)
          const da = vnode.component!.da
          da &amp;&amp; queuePostRenderEffect(da, suspense)
          return
      }
      unmount(cached)
      })
    })

    // setup返回的是一个函数,这个函数会被直接当做组件的渲染函数
    return () =&gt; {
      pendingCacheKey = null

      if (!slots.default) {
      // 无子节点
      return (current = null)
      }

      const children = slots.default()
      const rawVNode = children
      if (children.length &gt; 1) {
      // 只能有一个子节点
      if (__DEV__) {
          warn(`KeepAlive should contain exactly one component child.`)
      }
      current = null
      return children
      } else if (
      !isVNode(rawVNode) ||
      (!(rawVNode.shapeFlag &amp; ShapeFlags.STATEFUL_COMPONENT) &amp;&amp;
          !(rawVNode.shapeFlag &amp; ShapeFlags.SUSPENSE))
      ) {
      // 子节点必须是一个组件
      current = null
      return rawVNode
      }

      let vnode = getInnerChild(rawVNode)
   
      if (vnode.type === Comment) {
      current = null
      return vnode
      }

      const comp = vnode.type as ConcreteComponent

      // 获得组件的名称
      const name = getComponentName(
      isAsyncWrapper(vnode)
          ? (vnode.type as ComponentOptions).__asyncResolved || {}
          : comp,
      )

      const { include, exclude, max } = props
   
      // 根据include和exclude 判断当前的组件是否需要缓存
      if (
      (include &amp;&amp; (!name || !matches(include, name))) ||
      (exclude &amp;&amp; name &amp;&amp; matches(exclude, name))
      ) {
      // #11717
      vnode.shapeFlag &amp;= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
      current = vnode
      return rawVNode
      }
      
      // 组件的key
      const key = vnode.key == null ? comp : vnode.key
      // 根据key 获得已缓存的VNode
      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 &amp; ShapeFlags.SUSPENSE) {
          rawVNode.ssContent = vnode
      }
      }

      pendingCacheKey = key

      if (cachedVNode) {
      // 如果实例已被缓存
      // 复制DOM节点、组件实例
      vnode.el = cachedVNode.el
      vnode.component = cachedVNode.component
      if (vnode.transition) {
          // recursively update transition hooks on subTree
          setTransitionHooks(vnode, vnode.transition!)
      }
      // 标记组件是从缓存中恢复防止组件被重新mounted
      vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
      // 把缓存的key移动到队尾
      keys.delete(key)
      keys.add(key)
      } else {
      // 实例未被缓存
      keys.add(key)
      // 如果换成数量超过max,删除最早进入的实例
      if (max &amp;&amp; keys.size &gt; parseInt(max as string, 10)) {
          pruneCacheEntry(keys.values().next().value!)
      }
      }
      // 标记组件应该被缓存 防止组件被卸载
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      current = vnode
      // 返回子节点的vNode
      return isSuspense(rawVNode.type) ? rawVNode : vnode
    }
},
}</pre>
</div>
<div>
<div>
<p>通过源码的分析,可以回答上面的问题了 首先,KeepAlive的用法要符合它的规范,只能用它嵌套组件,并且只能嵌套一个子组件。 其次,如果设置了<code>include</code>和<code>exclude</code>限制,那么组件的名称必须要满足这些限制才会被缓存,且当前KeepAlive缓存的数量未超过<code>max</code>的限制。 KeepAlive的渲染函数最终渲染的是它默认插槽的内容,缓存的是组件的VNode。</p>
<h3 data-id="heading-4">3.组件切换如何触发所有子组件注册的onActivated和onDeactivated函数的</h3>
<p>想要回答好这个问题,也需要结合<code>onActivated</code>和<code>onDeactivated</code>函数的定义</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">export enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u',
BEFORE_UNMOUNT = 'bum',
UNMOUNTED = 'um',
// 组件实例上的da属性,代表是该组件注册的Deactivated函数
DEACTIVATED = 'da',
// 组件实例上的a属性,代表是该组件注册的Activated函数
ACTIVATED = 'a',
RENDER_TRIGGERED = 'rtg',
RENDER_TRACKED = 'rtc',
ERROR_CAPTURED = 'ec',
SERVER_PREFETCH = 'sp',
}

// 注册一个回调函数,若组件实例是&lt;KeepAlive&gt;缓存树的一部分,当组件被插入到 DOM 中时调用
export function onActivated(
hook: Function,
target?: ComponentInternalInstance | null,
): void {
registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
}

// 注册一个回调函数,若组件实例是&lt;KeepAlive&gt;缓存树的一部分,当组件从 DOM 中被移除时调用。
export function onDeactivated(
hook: Function,
target?: ComponentInternalInstance | null,
): void {
registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
}

function registerKeepAliveHook(
hook: Function &amp; { __wdc?: Function },
type: LifecycleHooks,
target: ComponentInternalInstance | null = currentInstance,
) {
   "__wdc" 代表 "with deactivation check(带有失活检查)".
const wrappedHook =
    hook.__wdc ||
    (hook.__wdc = () =&gt; {
      // 仅当目标实例不在失活分支中时,才触发挂钩.
      let current: ComponentInternalInstance | null = target
      while (current) {
      if (current.isDeactivated) {
          return
      }
      current = current.parent
      }
      return hook()
    })
// 在当前组件实例上注册该回调函数
injectHook(type, wrappedHook, target)
// 把回调注册到KeepAlive根组件上
// 避免了在调用这些钩子时遍历整个组件树的需要
if (target) {
    let current = target.parent
    while (current &amp;&amp; current.parent) {
      if (isKeepAlive(current.parent.vnode)) {
      injectToKeepAliveRoot(wrappedHook, type, target, current)
      }
      current = current.parent
    }
}
}

function injectToKeepAliveRoot(
hook: Function &amp; { __weh?: Function },
type: LifecycleHooks,
target: ComponentInternalInstance,
keepAliveRoot: ComponentInternalInstance,
) {
// 将钩子注册到KeepAlive根组件上, 注册到队头,优先与父组件同类型的钩子触发
const injected = injectHook(type, hook, keepAliveRoot, true /* prepend */)
// 当前组件卸载时,移除注册的钩子
onUnmounted(() =&gt; {
    remove(keepAliveRoot!, injected)
}, target)
}

// 注册钩子,所有的钩子包括onMounted在内的钩子最终都是通过这个钩子注册的
export function injectHook(
type: LifecycleHooks,
hook: Function &amp; { __weh?: Function },
target: ComponentInternalInstance | null = currentInstance,
prepend: boolean = false,
): Function | undefined {
if (target) {
    const hooks = target || (target = [])
    // “__weh”代表“with error handling(带有错误检查)”。
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) =&gt; {
      pauseTracking()
      const reset = setCurrentInstance(target)
      const res = callWithAsyncErrorHandling(hook, target, type, args)
      reset()
      resetTracking()
      return res
      })
    if (prepend) {
      // 插入到队头
      hooks.unshift(wrappedHook)
    } else {
      // 插入队尾
      hooks.push(wrappedHook)
    }
    // 返回最终插入的钩子函数
    return wrappedHook
}
}</pre>
</div>
<div>
<div>
<p>看了函数的定义后,可以回答上面的问题了 通过<code>onActivated</code>和<code>onDeactivated</code>注册的函数最终都会注册到<code>KeepAlive根组件</code>的实例的<code>a</code>和<code>da</code>属性上,这两个属性都是数组,并且子组件对应的函数会注册到<code>KeepAlive根组件</code>实例的<code>a</code>和<code>da</code>属性的队头,优先于父组件注册的同类型的钩子执行。等<code>KeepAlive根组件</code>切换时,只需要按需调用根组件实例上的<code>a</code>和<code>da</code>中所有的函数即可。</p>
<h3 data-id="heading-5">4.KeepAlive是如何处理组件的移除和恢复的</h3>
<h4 data-id="heading-6">4.1移除流程</h4>
<p>先回头看看<code>KeepAlive</code>组件的渲染函数,它在渲染需要缓存的子组件时,会给它的VNode设置一个标记<code>COMPONENT_SHOULD_KEEP_ALIVE</code>,表示这个组件需要被缓存,这个标记会在两个地方会被用到,一是组件的初次渲染,二是组件卸载,接下来我们来分别看看这两个地方具体是干了什么。</p>
<h5 data-id="heading-7">4.1.1初次渲染</h5>
<p>初次渲染的核心流程代码在<code>runtime-core/src/renderer.ts</code>这个文件中,这个文件里面包含了<code>VNode patch</code>的核心流程。组件初次渲染的函数调用顺序是<em>patch -&gt; processComponent -&gt; mountComponent -&gt; setupRenderEffect -&gt; componentUpdateFn</em>,我们直接来看<code>componentUpdateFn</code>中的部分定义,因为<code>COMPONENT_SHOULD_KEEP_ALIVE</code>在这个方法中被用到了。</p>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
namespace: ElementNamespace,
optimized,
) =&gt; {
const componentUpdateFn = () =&gt; {
    if (!instance.isMounted) {
      // 组件未挂载
      let vnodeHook: VNodeHook | null | undefined
      const { el, props } = initialVNode
      const { bm, m, parent, root, type } = instance
      const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

      if (bm) {
      // 调用beforeMount钩子函数
      invokeArrayFns(bm)
      }

      //
      // 中间挂载过程中的代码
      //

      if (m) {
      // 挂载完成后调用mounted钩子函数
      queuePostRenderEffect(m, parentSuspense)
      }

      if (
      initialVNode.shapeFlag &amp; ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
      (parent &amp;&amp;
          isAsyncWrapper(parent.vnode) &amp;&amp;
          parent.vnode.shapeFlag &amp; ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
      ) {
      // 组件初次渲染后,如果是被keep-alive包裹的组件,则调用activated钩子函数
      instance.a &amp;&amp; queuePostRenderEffect(instance.a, parentSuspense)
      }
      // 标记组件已挂载
      instance.isMounted = true
    } else {
      // 组件已挂载
    }
}
}</pre>
</div>
<p>可以看到他的第一个作用就是在组件初次渲染后,如果是被keep-alive包裹的组件,则调用activated钩子函数。</p>
<h4 data-id="heading-8">4.1.2组件卸载</h4>
<p>vnode卸载时会统一调用<code>renderer.ts</code>中定义的<code>unmount</code>方法</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false,
) =&gt; {
const { type, props, ref, children, dynamicChildren, shapeFlag, patchFlag, dirs, cacheIndex } =
    vnode

// 卸载vnode时,如果组件被keepalive缓存,则调用keepalive内部定义的deactivate方法
if (shapeFlag &amp; ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
    return
}

// 省去其他代码
}</pre>
</div>
<p>我们可以看到在卸载时,如果遇到了这个标记就不会继续执行后续的卸载逻辑,而是调用了<code>KeepAlive</code>内部定义的<code>deactivate</code>方法。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const storageContainer = createElement('div')

sharedContext.deactivate = (vnode: VNode) =&gt; {
const instance = vnode.component!

// 把组件的节点从当前页面上 移动到临时的容器节点中
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
queuePostRenderEffect(() =&gt; {
    if (instance.da) {
      // 调用组件中定义的onDeactivated的钩子函数
      invokeArrayFns(instance.da)
    }
    const vnodeHook = vnode.props &amp;&amp; vnode.props.onVnodeUnmounted
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode)
    }
    // 标记组件已失活
    instance.isDeactivated = true
}, parentSuspense)
}</pre>
</div>
<div>
<div>
<p>通过不断深入分析,发现被缓存的组件卸载时,只会将他的Dom元素移动到临时创建的div中,并且调用组件中定义的<code>onDeactivated</code>的钩子函数</p>
<h4 data-id="heading-9">4.2恢复流程</h4>
<p>还是回到<code>KeepAlive</code>的渲染函数,vnode要复用的时候,他会给vnode标记为<code>COMPONENT_KEPT_ALIVE</code>,表示这个组件是被缓存的组件。这个标记也是会在后面的<code>patch</code>流程中被使用到,组件恢复时的函数调用顺序是<em>patch -&gt; processComponent</em></p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
) =&gt; {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      // 无旧vNode
      if (n2.shapeFlag &amp; ShapeFlags.COMPONENT_KEPT_ALIVE) {
      // 被缓存的组件,则调用keepalive内部定义的activate方法
      ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          namespace,
          optimized,
      )
      } else {
       // 未被缓存的组件 重新挂载
      mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          optimized,
      )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
}</pre>
</div>
可以看到在重新渲染时,如果遇到了这个标记就不会重新走初次挂载逻辑,而是调用了<code>KeepAlive</code>内部定义的<code>activate</code>方法。<br>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">    sharedContext.activate = (
      vnode,
      container,
      anchor,
      namespace,
      optimized,
    ) =&gt; {
      const instance = vnode.component!
      // 将Dom节点先恢复到页面中
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // 还需要patch更新流程,因为组件的props可能会发生变化
      patch(
      instance.vnode,
      vnode,
      container,
      anchor,
      instance,
      parentSuspense,
      namespace,
      vnode.slotScopeIds,
      optimized,
      )
      queuePostRenderEffect(() =&gt; {
      instance.isDeactivated = false
      if (instance.a) {
          // 调用组件中定义的onActivated的钩子函数
          invokeArrayFns(instance.a)
      }
      const vnodeHook = vnode.props &amp;&amp; vnode.props.onVnodeMounted
      if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
      }
      }, parentSuspense)
    }</pre>
</div>
<div>
<div>
<p>通过不断深入分析,发现被缓存的组件恢复时,只会将他的Dom元素重新移动到页面中,防止组件的<code>props</code>变化还需要走一遍<code>patch</code>更新流程(关于为什么要<code>patch</code>,下一个问题中会提到),最后调用组件中定义的<code>onActivated</code>的钩子函数。</p>
<p>通过代码的层层分析,可算是弄懂了<code>KeepAlive</code>是如何处理组件的移除和恢复的了。</p>
<h3 data-id="heading-10">5.失活的组件,依赖的数据更新了,它会重新渲染吗</h3>
<p>这个问题要分两种况讨论,第一种是由父组件传入到子组件的数据<code>props</code>,第二种是组件自身内部的定义的数据或依赖的全局状态</p>
<h4 data-id="heading-11">5.1 父组件传入的props变化</h4>
<p>要理解父组件传入的<code>props</code>变化,会不会触发失活的组件更新,我们需要知道子组件是如何使用父组件传入的<code>props</code>的,所以还是需要结合一下相关的代码做分析。<br>
首先父组件传递给子组件的<code>props</code>是先绑定在子组件的<code>VNode</code>上面的,类似下面这样</p>

</div>

</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// 模板中这样写 传递count给子组件
&lt;CompChild :count="count"&gt;&lt;/CompChild&gt;

// 实际在渲染函数大概会长这样
// 第一个参数就是组件本升,第二个参数就是传递给组件的props
_createBlock(_component_CompChild, { count: $setup.count }, null, 8 /* PROPS */, ["count"])

// 生成的VNode 大概会长这样
{
   // 标记这是一个VNode
   __v_isVNode: true,
   // 类型,组件编译后的js对象
   type: _component_CompChild,
   // props
   props: { count },
   // 动态参数
   dynamicProps: ['count'],
   key: null,
   // 组件实例
   component: null,
   // 渲染的dom
   el: null,
   patchFlag: 8,
   children: null
   // 还有一些其他属性
}</pre>
</div>
<p>传给组件的参数会被记录在组件<code>VNode</code>的<code>props</code>属性上,等接下来组件渲染的时候会被初始化到组件的实例上面。 组件初始化的时候会调用<code>setupComponent</code>这个方法,该方法定义在<code>runtime-core/src/component.ts</code>中</p>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// component.ts
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false,
optimized = false,
): Promise&lt;void&gt; | undefined {
isSSR &amp;&amp; setInSSRSetupState(isSSR)

const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
// 初始化组件props
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children, optimized || isSSR)

// 执行setup函数,并处理setup结果
const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined

isSSR &amp;&amp; setInSSRSetupState(false)
return setupResult
}

// componentProps.ts
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
isStateful: number, // result of bitwise flag comparison
isSSR = false,
): void {
const props: Data = {}
const attrs: Data = createInternalObject()
instance.propsDefaults = Object.create(null)

setFullProps(instance, rawProps, props, attrs)

if (isStateful) {
    // 组件实例上的props是在VNode的props基础上包了一层shallowReactive浅层响应式
    instance.props = isSSR ? props : shallowReactive(props)
} else {
   // 函数式组件
}
instance.attrs = attrs
}</pre>
</div>
<div>
<div>
<p>所以传递给子组件的<code>props</code>最终会被赋值给组件实例的<code>props</code>属性,并且会被转换成浅层响应式数据。最终组件的渲染函数和setup函数用的也是被转换后的<code>props</code>。</p>
<p>当传递给组件的<code>props</code>变化的时候,首先会触发父组件的的render函数重新运行,然后会生成新的子组件的<code>VNode</code>,然后就会进入<code>patch</code>更新流程,这时候子组件的<code>新旧Vnode</code>对比,新旧对比会调用<code>updateProps</code>更新组件的props</p>
</div>
</div>
<div class="cnblogs_Highlighter">
<pre class="brush:csharp;gutter:true;">// renderer.ts
const updateComponentPreRender = (
    instance: ComponentInternalInstance,
    nextVNode: VNode,
    optimized: boolean,
) =&gt; {
    nextVNode.component = instance
    const prevProps = instance.vnode.props
    instance.vnode = nextVNode
    instance.next = null
    // 更新组件的参数
    updateProps(instance, nextVNode.props, prevProps, optimized)
    updateSlots(instance, nextVNode.children, optimized)

    pauseTracking()
    // props更新后 先触发pre-flush watchers
    flushPreFlushCbs(instance)
    resetTracking()
}

// componentProps.ts
export function updateProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
rawPrevProps: Data | null,
optimized: boolean,
): void {
   // 更新props的函数,这个函数代码较多,就不贴了
   // 这个函数里面会把组件实例的props属性里面的值更新成新的值
}</pre>
</div>
<div>
<div>
<p>看完props的更新逻辑,就能回答上面的问题了,组件内部使用的<code>props</code>必须要<code>patch</code>完成之后才会变成最新的数据,所以在组件失活的时候,即使父组件传入的<code>props</code>发生了变化,但是由于子组件内部使用的<code>props</code>数据并没有发生变化,所以这时候子组件是不会重新渲染的,只有等组件重新恢复的时候,手动的调用<code>patch</code>,完成<code>props</code>更新,如果<code>props</code>发生变化才会触发子组件的重新渲染。</p>
<h4 data-id="heading-12">5.2 全局状态变化</h4>
<p>相比较于<code>props</code>的更新逻辑,全局状态变化会很好理解,其实就是依赖收集和派发更新的逻辑 只要组件内部依赖的任何状态更新了,就会触发组件的重新渲染,无论组件是否失活。</p>
<p>所以第五个问题的结论也显而易见了,只有组件依赖的数据是父组件传入的<code>props</code>并且这个<code>props</code>传入的只是<code>原始类型</code>或者说是非响应式的数据对象,这时候即使外部数据发生了变化,那么子组件在失活的时候是不会触发重新渲染的,而除了这种情况以外,依赖的任何其他的响应式数据发生变化都是会触发组件重新渲染的。</p>
<h2 data-id="heading-13">遇到过的问题</h2>
<h3 data-id="heading-14">失活组件重新渲染导致的BUG</h3>
<p>在第四个问题中讨论过失活组件的DOM节点会被移动到一个临时创建的<code>div</code>中,这个时候虽然DOM没有被销毁,但是DOM的父级已经变了。 说一个场景,比如我们在项目中大量的使用了<code>Vant</code>中的<code>List</code>和<code>Sticky</code>这些组件,这些组件有一个特点他会去寻找最近的父级滚动节点作为滚动监听的对象,由于失活的组件DOM已经被移动到其他的地方,然而组件更新还是会正常触发,这时候<code>List</code>这类组件寻找最近的父级滚动节点可能会找的不对,所以等组件再次恢复时,可能就会看到<code>List</code>的上拉加载回调不会被执行了这么一个奇怪的BUG。</p>
<h2 data-id="heading-15">结语</h2>
<p>能够彻底弄明白上面的所有问题,不仅可以把<code>KeepAlive</code>弄明白了,而且顺带的组件的渲染流程也能掌握的八九不离十了。</p>
</div>
<div>
<h3 id="tid-D8HBxE">如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。</h3>
</div>
<p><em><img src="https://img2024.cnblogs.com/blog/2149129/202501/2149129-20250122165814748-630765389.png" alt="" loading="lazy"></em></p>
</div>
</div><br><br>
来源:https://www.cnblogs.com/smileZAZ/p/19468121
頁: [1]
查看完整版本: 彻底弄懂KeepAlive