Explore the implementation logic of Vue3's keep alive and dynamic components

Keep alive component is a component provided by Vue. It can cache component instances and avoid component mounting and unloading in some cases. It is very practical in some scenarios.

For example, we recently encountered a scenario in which a component uploads a large file is a time-consuming operation. If you switch to other page contents during uploading, the component will be unloaded and the corresponding download will be cancelled. At this time, you can wrap this component with the keep alive component. When you switch to other pages, the component can still continue to upload files, and you can also see the upload progress when you switch back.

keep-alive

Render child nodes
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,

  setup(props: KeepAliveProps, { slots }: SetupContext) {

    // Subtree VNode to render
    let current: VNode | null = null

    return () => {

      // Get the child node. Because keep alive can only have one child node, directly take the first child node
      const children = slots.default()
      const rawVNode = children[0]

      // Tag | shapeflags COMPONENT_ SHOULD_ KEEP_ Alive, this component is a 'keep alive' component. This tag does not follow unmount logic because it needs to be cached
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      // Record current child node
      current = vnode

      // Returns a child node that represents the rendering of this child node
      return rawVNode
    }
  }
}

The setup return function of the component, which is the rendering function of the component;
Keep alive is a virtual node. You don't need to render, you only need to render the child node, so the function only needs to return the child node VNode.

Cache function
  • Define the Map for storing cached data. All cache key array Keys represent the cache key value pendingCacheKey of the current sub component;
const cache = new Map()
const keys: Keys = new Set()
let pendingCacheKey: CacheKey | null = null
  • Get the key of the sub tree node VNode in the rendering function, and check whether there is a cache node corresponding to the key in the cache
const key = vnode.key
const cachedVNode = cache.get(key)

key is added when generating the rendering function of child nodes. Generally, it is 0, 1, 2.

  • Record the key before the point
pendingCacheKey = key
  • If the cached cachedVNode node is found, copy the component instances and node elements of the cached cachedVNode node to the new VNode node. If not found, first add the pendingCacheKey of the current subtree node VNode to the Keys.
if (cachedVNode) {
  // Replication node
  vnode.el = cachedVNode.el
  vnode.component = cachedVNode.component
  // Tag | shapeflags COMPONENT_ KEPT_ Alive, this component is a reusable 'VNode', and this tag does not follow the mount logic
  vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
} else {
  // Add pendingCacheKey
  keys.add(key)
}

Question: why not store {pendingCacheKey: vnode} in the cache?
Answer: in fact, this logic can be added here, but the logic of official interval is delayed. I don't think it makes any difference.

  • Add / update the cache when the component mounts onMounted and updates onUpdated
onMounted(cacheSubtree)
onUpdated(cacheSubtree)

const cacheSubtree = () => {
  if (pendingCacheKey != null) {
    // Add / update cache
    cache.set(pendingCacheKey, instance.subTree)
  }
}

All codes
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,

  setup(props: KeepAliveProps, { slots }: SetupContext) {

    let current: VNode | null = null
    // Some cached data
    const cache = new Map()
    const keys: Keys = new Set()
    let pendingCacheKey: CacheKey | null = null

    // Update / add cached data
    const cacheSubtree = () => {
      if (pendingCacheKey != null) {
        // Add / update cache
        cache.set(pendingCacheKey, instance.subTree)
      }
    }

    // Listening lifecycle
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

    return () => {
      const children = slots.default()
      const rawVNode = children[0]

      // Get cache
      const key = rawVNode.key
      const cachedVNode = cache.get(key)

      pendingCacheKey = key

      if (cachedVNode) {
        // Reuse DOM and component instances
        rawVNode.el = cachedVNode.el
        rawVNode.component = cachedVNode.component
      } else {
        // Add pendingCacheKey
        keys.add(key)
      }

      rawVNode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
      current = rawVNode
      return rawVNode
    }
  }
}

So far, the caching of DOM and component instances is realized through cache.

Keep alive patch reuse logic

We know that after generating VNode, patch logic is used to generate DOM.

const processComponent = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  n2.slotScopeIds = slotScopeIds
  if (n1 == null) {
    if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
      ;(parentComponent!.ctx as KeepAliveContext).activate(
        n2,
        container,
        anchor,
        isSVG,
        optimized
      )
    } else {
      mountComponent(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
  }
}

If shapeflags are reused when processComponent processes component logic COMPONENT_ KEPT_ Alive follows the activate method of the parent component keep alive;

const unmount: UnmountFn = (
  vnode,
  parentComponent,
  parentSuspense,
  doRemove = false,
  optimized = false
) => {
  const {
    type,
    props,
    ref,
    children,
    dynamicChildren,
    shapeFlag,
    patchFlag,
    dirs
  } = vnode
  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
    return
  }
}

unmount unmounted keep alive component shapeflags COMPONENT_ SHOULD_ KEEP_ When alive, call the deactivate method of the parent component keep alive.

Summary: the reuse and unloading of keep alive components are taken over by the activate method and the deactivate method.

active logic
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
  const instance = vnode.component!
  // 1. Mount DOM directly
  move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
  // 2. Update prop
  patch(
    instance.vnode,
    vnode,
    container,
    anchor,
    instance,
    parentSuspense,
    isSVG,
    vnode.slotScopeIds,
    optimized
  )
  // 3. Execute onVnodeMounted hook function asynchronously
  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)

}
  1. Mount DOM directly
  2. Update prop
  3. Execute onVnodeMounted hook function asynchronously
deactivate logic
const storageContainer = createElement('div')

sharedContext.deactivate = (vnode: VNode) => {
  const instance = vnode.component!
  // 1. Remove the DOM and mount it under a new div
  move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
  // 2. Execute onVnodeUnmounted hook function asynchronously
  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)
  }
}
  1. Remove the DOM and mount it under a new div
  2. Execute onVnodeUnmounted hook function asynchronously

Question: who will execute the deactivate of the old node and the active of the new node first
Answer: the deactivate of the old node is executed first, and the active of the new node is executed later.

unmount logic of keep alive
  • Unload all nodes except the current subtree VNode node in the cache, and the current component cancels the mark of keep alive, so that the current subtree VNode will be unloaded with keep alive.
onBeforeUnmount(() => {
  cache.forEach(cached => {
    const { subTree, suspense } = instance
    const vnode = getInnerChild(subTree)
    if (cached.type === vnode.type) {
      // Of course, the component first cancels the mark of 'keep alive', which can be unmount
      resetShapeFlag(vnode)
      // but invoke its deactivated hook here
      const da = vnode.component!.da
      da && queuePostRenderEffect(da, suspense)
      return
    }
    // Execute unmount method for each cached VNode
    unmount(cached)
  })
})

<!-- implement unmount -->
function unmount(vnode: VNode) {
    // Cancel the mark of 'keep alive' to unmount the execution
    resetShapeFlag(vnode)
    // unmout
    _unmount(vnode, instance, parentSuspense)
}

Keep alive is unloaded, and its cached DOM will also be unloaded.

The configuration of keep alive cache includes, excludes and max

It's good to know the logic in this part, without code analysis.

  1. Components with component names in include will be cached;
  2. Components with component names in exclude will not be cached;
  3. Specify the maximum number of caches. If it exceeds, delete the first content of the cache.

Dynamic component

usage method
<keep-alive>
  <component is="A"></component>
</keep-alive>
Rendering function
resolveDynamicComponent("A")
Logic of resolveDynamicComponent
export function resolveDynamicComponent(component: unknown): VNodeTypes {
  if (isString(component)) {
    return resolveAsset(COMPONENTS, component, false) || component
  }
}

function resolveAsset(
  type,
  name,
  warnMissing = true,
  maybeSelfReference = false
) {
  const res =
    // local registration
    // check instance[type] first which is resolved for options API
    resolve(instance[type] || Component[type], name) ||
    // global registration
    resolve(instance.appContext[type], name)
  return res
}

Like the directive, resolvedynamic component is to find the locally or globally registered component according to its name, and then render the corresponding component.

Keywords: Javascript Front-end Vue Vue.js

Added by joe1986 on Mon, 31 Jan 2022 13:49:52 +0200