彻底弄懂KeepAlive

前言

开发过Vue应用的同学对KeepAlive功能应该都不陌生了,但是大家对它的理解是只停留在知道怎么用的阶段 还是说清晰的知道它内部的实现细节呢,在项目中因KeepAlive导致的的Bug能第一时间分析出来原因并且找到解决方法呢。这篇文章的目的就是想结合Vue渲染的核心细节来重新认识一下KeepAlive这个功能。

文章是基于Vue3.5.24版本做的分析

接下来我将通过对几个问题的解释,来慢慢梳理KeepAlive的细节。

带着问题弄懂KeepAlive

1.编写的Vue文件在浏览器运行时是什么样子的?

看一个下面的简单例子

typescript 复制代码
<template>
  <div>{{ count }}</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const count = ref(0)
</script>

我们写的这么简单的一段代码,在运行前会被编译成下面这个样子,传送门Vue SFC Playground

typescript 复制代码
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__

通过结果可以看到,Vue文件中的内容编译后变成了一个普通的JS对象。

其中主要包含以下几个属性

_name: 组件的名称,未明确定义组件名称的情况会使用文件的名称作为组件的名称。

setup: 组件中定义的setup函数,默认返回了定义的响应式数据。

render: 渲染函数,通过将template模板编译而来,返回值是一个VNode。

_file: 组件的源文件名称。

2.组件需要满足什么条件才会被缓存,缓存的是什么?

想要回答好这个问题,就需要结合KeepAlive组件的源码。

typescript 复制代码
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,

  __isKeepAlive: true,

  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number],
  },

  setup(props: KeepAliveProps, { slots }: SetupContext) {
    const instance = getCurrentInstance()!
 
    const sharedContext = instance.ctx as KeepAliveContext

    if (__SSR__ && !sharedContext.renderer) {
      return () => {
        const children = slots.default && slots.default()
        return children && children.length === 1 ? children[0] : children
      }
    }

    // key -> vNode  Map结构
    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,
    ) => {
      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(() => {
        instance.isDeactivated = false
        if (instance.a) {
          invokeArrayFns(instance.a)
        }
        const vnodeHook = vnode.props && 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) => {
      const instance = vnode.component!
      invalidateMount(instance.m)
      invalidateMount(instance.a)

      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      queuePostRenderEffect(() => {
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        const vnodeHook = vnode.props && 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__ && __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) => boolean) {
      cache.forEach((vnode, key) => {
        const name = getComponentName(vnode.type as ConcreteComponent)
        if (name && !filter(name)) {
          pruneCacheEntry(key)
        }
      })
    }

    // 根据key移除缓存的实例
    function pruneCacheEntry(key: CacheKey) {
      const cached = cache.get(key) as VNode
      if (cached && (!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(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        include && pruneCache(name => matches(include, name))
        exclude && pruneCache(name => !matches(exclude, name))
      },
      // prune post-render after `current` has been updated
      { flush: 'post', deep: true },
    )

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

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

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

      if (!slots.default) {
        // 无子节点
        return (current = 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)
     
      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 && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        // #11717
        vnode.shapeFlag &= ~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 & 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 && keys.size > 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
    }
  },
}

通过源码的分析,可以回答上面的问题了 首先,KeepAlive的用法要符合它的规范,只能用它嵌套组件,并且只能嵌套一个子组件。 其次,如果设置了includeexclude限制,那么组件的名称必须要满足这些限制才会被缓存,且当前KeepAlive缓存的数量未超过max的限制。 KeepAlive的渲染函数最终渲染的是它默认插槽的内容,缓存的是组件的VNode。

3.组件切换如何触发所有子组件注册的onActivated和onDeactivated函数的

想要回答好这个问题,也需要结合onActivatedonDeactivated函数的定义

typescript 复制代码
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',
}

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

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

function registerKeepAliveHook(
  hook: Function & { __wdc?: Function },
  type: LifecycleHooks,
  target: ComponentInternalInstance | null = currentInstance,
) {
   "__wdc" 代表 "with deactivation check(带有失活检查)".
  const wrappedHook =
    hook.__wdc ||
    (hook.__wdc = () => {
      // 仅当目标实例不在失活分支中时,才触发挂钩.
      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 && current.parent) {
      if (isKeepAlive(current.parent.vnode)) {
        injectToKeepAliveRoot(wrappedHook, type, target, current)
      }
      current = current.parent
    }
  }
}

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

// 注册钩子,所有的钩子包括onMounted在内的钩子最终都是通过这个钩子注册的
export function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false,
): Function | undefined {
  if (target) {
    const hooks = target[type] || (target[type] = [])
    // "__weh"代表"with error handling(带有错误检查)"。
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) => {
        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
  } 
}

看了函数的定义后,可以回答上面的问题了 通过onActivatedonDeactivated注册的函数最终都会注册到KeepAlive根组件的实例的ada属性上,这两个属性都是数组,并且子组件对应的函数会注册到KeepAlive根组件实例的ada属性的队头,优先于父组件注册的同类型的钩子执行。等KeepAlive根组件切换时,只需要按需调用根组件实例上的ada中所有的函数即可。

4.KeepAlive是如何处理组件的移除和恢复的

4.1移除流程

先回头看看KeepAlive组件的渲染函数,它在渲染需要缓存的子组件时,会给它的VNode设置一个标记COMPONENT_SHOULD_KEEP_ALIVE,表示这个组件需要被缓存,这个标记会在两个地方会被用到,一是组件的初次渲染,二是组件卸载,接下来我们来分别看看这两个地方具体是干了什么。

4.1.1初次渲染

初次渲染的核心流程代码在runtime-core/src/renderer.ts这个文件中,这个文件里面包含了VNode patch的核心流程。组件初次渲染的函数调用顺序是patch -> processComponent -> mountComponent -> setupRenderEffect -> componentUpdateFn ,我们直接来看componentUpdateFn中的部分定义,因为COMPONENT_SHOULD_KEEP_ALIVE在这个方法中被用到了。

typescript 复制代码
const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  namespace: ElementNamespace,
  optimized,
) => {
  const componentUpdateFn = () => {
    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 & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
        (parent &&
          isAsyncWrapper(parent.vnode) &&
          parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
      ) {
        // 组件初次渲染后,如果是被keep-alive包裹的组件,则调用activated钩子函数
        instance.a && queuePostRenderEffect(instance.a, parentSuspense)
      }
      // 标记组件已挂载
      instance.isMounted = true
    } else {
      // 组件已挂载
    }
  }
}

可以看到他的第一个作用就是在组件初次渲染后,如果是被keep-alive包裹的组件,则调用activated钩子函数。

4.1.2组件卸载

vnode卸载时会统一调用renderer.ts中定义的unmount方法

typescript 复制代码
const unmount: UnmountFn = (
  vnode,
  parentComponent,
  parentSuspense,
  doRemove = false,
  optimized = false,
) => {
  const { type, props, ref, children, dynamicChildren, shapeFlag, patchFlag, dirs, cacheIndex } =
    vnode

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

  // 省去其他代码
}

我们可以看到在卸载时,如果遇到了这个标记就不会继续执行后续的卸载逻辑,而是调用了KeepAlive内部定义的deactivate方法。

typescript 复制代码
const storageContainer = createElement('div')

sharedContext.deactivate = (vnode: VNode) => {
  const instance = vnode.component!

  // 把组件的节点从当前页面上 移动到临时的容器节点中
  move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
  queuePostRenderEffect(() => {
    if (instance.da) {
      // 调用组件中定义的onDeactivated的钩子函数
      invokeArrayFns(instance.da)
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode)
    }
    // 标记组件已失活
    instance.isDeactivated = true
  }, parentSuspense)
}

通过不断深入分析,发现被缓存的组件卸载时,只会将他的Dom元素移动到临时创建的div中,并且调用组件中定义的onDeactivated的钩子函数

4.2恢复流程

还是回到KeepAlive的渲染函数,vnode要复用的时候,他会给vnode标记为COMPONENT_KEPT_ALIVE,表示这个组件是被缓存的组件。这个标记也是会在后面的patch流程中被使用到,组件恢复时的函数调用顺序是patch -> processComponent

typescript 复制代码
  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,
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      // 无旧vNode
      if (n2.shapeFlag & 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)
    }
  }

可以看到在重新渲染时,如果遇到了这个标记就不会重新走初次挂载逻辑,而是调用了KeepAlive内部定义的activate方法。

typescript 复制代码
    sharedContext.activate = (
      vnode,
      container,
      anchor,
      namespace,
      optimized,
    ) => {
      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(() => {
        instance.isDeactivated = false
        if (instance.a) {
          // 调用组件中定义的onActivated的钩子函数
          invokeArrayFns(instance.a)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
      }, parentSuspense)
    }

通过不断深入分析,发现被缓存的组件恢复时,只会将他的Dom元素重新移动到页面中,防止组件的props变化还需要走一遍patch更新流程(关于为什么要patch,下一个问题中会提到),最后调用组件中定义的onActivated的钩子函数。

通过代码的层层分析,可算是弄懂了KeepAlive是如何处理组件的移除和恢复的了。

5.失活的组件,依赖的数据更新了,它会重新渲染吗

这个问题要分两种况讨论,第一种是由父组件传入到子组件的数据props,第二种是组件自身内部的定义的数据或依赖的全局状态

5.1 父组件传入的props变化

要理解父组件传入的props变化,会不会触发失活的组件更新,我们需要知道子组件是如何使用父组件传入的props的,所以还是需要结合一下相关的代码做分析。

首先父组件传递给子组件的props是先绑定在子组件的VNode上面的,类似下面这样

typescript 复制代码
// 模板中这样写 传递count给子组件
<CompChild :count="count"></CompChild>

// 实际在渲染函数大概会长这样
// 第一个参数就是组件本升,第二个参数就是传递给组件的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
   // 还有一些其他属性
}

传给组件的参数会被记录在组件VNodeprops属性上,等接下来组件渲染的时候会被初始化到组件的实例上面。 组件初始化的时候会调用setupComponent这个方法,该方法定义在runtime-core/src/component.ts

typescript 复制代码
// component.ts
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false,
  optimized = false,
): Promise<void> | undefined {
  isSSR && 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 && 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
}

所以传递给子组件的props最终会被赋值给组件实例的props属性,并且会被转换成浅层响应式数据。最终组件的渲染函数和setup函数用的也是被转换后的props

当传递给组件的props变化的时候,首先会触发父组件的的render函数重新运行,然后会生成新的子组件的VNode,然后就会进入patch更新流程,这时候子组件的新旧Vnode对比,新旧对比会调用updateProps更新组件的props

typescript 复制代码
  // renderer.ts
  const updateComponentPreRender = (
    instance: ComponentInternalInstance,
    nextVNode: VNode,
    optimized: boolean,
  ) => {
    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属性里面的值更新成新的值
}

等子组件的props更新后,会重新调用子组件的render函数,重新生成子组件的内部结构的VNode,然后接着走子组件内部结构的patch流程

typescript 复制代码
export function renderComponentRoot(
  instance: ComponentInternalInstance,
): VNode {
  const {
    type: Component,
    vnode,
    proxy,
    withProxy,
    propsOptions: [propsOptions],
    slots,
    attrs,
    emit,
    render,
    renderCache,
    props,
    data,
    setupState,
    ctx,
    inheritAttrs,
  } = instance

  let result


  try {
    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      const proxyToUse = withProxy || proxy

      const thisProxy =
        __DEV__ && setupState.__isScriptSetup
          ? new Proxy(proxyToUse!, {
              get(target, key, receiver) {
                warn(
                  `Property '${String(
                    key,
                  )}' was accessed via 'this'. Avoid using 'this' in templates.`,
                )
                return Reflect.get(target, key, receiver)
              },
            })
          : proxyToUse
      // 执行render函数,获取新的vnode
      result = normalizeVNode(
        render!.call(
          thisProxy,
          proxyToUse!,
          renderCache,
          // 开发模式下,props是shallowReadonly的,所以子组件内部无法修改props
          __DEV__ ? shallowReadonly(props) : props,
          setupState,
          data,
          ctx,
        ),
      )
      fallthroughAttrs = attrs
    } else {
      // functional
  } catch (err) {
    //
  }

  // 省去了一大堆代码
}

看完props的更新逻辑,就能回答上面的问题了,组件内部使用的props必须要patch完成之后才会变成最新的数据,所以在组件失活的时候,即使父组件传入的props发生了变化,但是由于子组件内部使用的props数据并没有发生变化,所以这时候子组件是不会重新渲染的,只有等组件重新恢复的时候,手动的调用patch,完成props更新,如果props发生变化才会触发子组件的重新渲染。

5.2 全局状态变化

相比较于props的更新逻辑,全局状态变化会很好理解,其实就是依赖收集和派发更新的逻辑 只要组件内部依赖的任何状态更新了,就会触发组件的重新渲染,无论组件是否失活。

所以第五个问题的结论也显而易见了,只有组件依赖的数据是父组件传入的props并且这个props传入的只是原始类型或者说是非响应式的数据对象,这时候即使外部数据发生了变化,那么子组件在失活的时候是不会触发重新渲染的,而除了这种情况以外,依赖的任何其他的响应式数据发生变化都是会触发组件重新渲染的。

遇到过的问题

失活组件重新渲染导致的BUG

在第四个问题中讨论过失活组件的DOM节点会被移动到一个临时创建的div中,这个时候虽然DOM没有被销毁,但是DOM的父级已经变了。 说一个场景,比如我们在项目中大量的使用了Vant中的ListSticky这些组件,这些组件有一个特点他会去寻找最近的父级滚动节点作为滚动监听的对象,由于失活的组件DOM已经被移动到其他的地方,然而组件更新还是会正常触发,这时候List这类组件寻找最近的父级滚动节点可能会找的不对,所以等组件再次恢复时,可能就会看到List的上拉加载回调不会被执行了这么一个奇怪的BUG。

结语

能够彻底弄明白上面的所有问题,不仅可以把KeepAlive弄明白了,而且顺带的组件的渲染流程也能掌握的八九不离十了。

相关推荐
小胖霞1 小时前
彻底搞懂 JWT 登录认证与路由守卫(五)
前端·vue.js·node.js
灵魂学者1 小时前
Vue3.x —— ref 的使用
前端·javascript·vue.js
一 乐1 小时前
鲜花销售|基于springboot+vue的鲜花销售系统设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·spring
梦6501 小时前
VUE树形菜单组件如何实现展开/收起、全选/取消功能
前端·javascript·vue.js
我命由我123451 小时前
微信小程序 - 避免在 data 初始化中引用全局变量
开发语言·前端·javascript·微信小程序·小程序·前端框架·js
低保和光头哪个先来2 小时前
基于 Vue3 + Electron 的离线图片缓存方案
前端·javascript·electron
国服第二切图仔2 小时前
Electron for 鸿蒙PC项目实战之拖拽组件示例
javascript·electron·harmonyos
天天向上10242 小时前
Vue 配置一次打包执行多个命令,并将分别输出到不同的文件夹
前端·javascript·vue.js