探究Vue3的keep-alive和動態(tài)組件的實現(xiàn)邏輯

keep-alive組件是Vue提供的組件,它可以緩存組件實例,在某些情況下避免了組件的掛載和卸載,在某些場景下非常實用。

例如最近我們遇到了一種場景,某個組件上傳較大的文件是個耗時的操作,如果上傳的時候切換到其他頁面內(nèi)容,組件會被卸載,對應(yīng)的下載也會被取消。此時可以用keep-alive組件包裹這個組件,在切換到其他頁面時該組件仍然可以繼續(xù)上傳文件,切換回來也可以看到上傳進度。

keep-alive

渲染子節(jié)點
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,

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

    // 需要渲染的子樹VNode
    let current: VNode | null = null

    return () => {

      // 獲取子節(jié)點, 由于Keep-alive只能有一個子節(jié)點,直接取第一個子節(jié)點
      const children = slots.default()
      const rawVNode = children[0]

      // 標記 | ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE,這個組件是`keep-alive`組件, 這個標記 不走 unmount邏輯,因為要被緩存的
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      // 記錄當前子節(jié)點
      current = vnode

      // 返回子節(jié)點,代表渲染這個子節(jié)點
      return rawVNode
    }
  }
}

組件的setup返回函數(shù),這個函數(shù)就是組件的渲染函數(shù);
keep-alive是一個虛擬節(jié)點不需要渲染,只需要渲染子節(jié)點,所以函數(shù)只需要返回子節(jié)點VNode就行了。

緩存功能
  • 定義存儲緩存數(shù)據(jù)的Map, 所有的緩存鍵值數(shù)組Keys,代表當前子組件的緩存鍵值pendingCacheKey;
const cache = new Map()
const keys: Keys = new Set()
let pendingCacheKey: CacheKey | null = null
  • 渲染函數(shù)中獲取子樹節(jié)點VNodekey, 緩存cache中查看是否有key對應(yīng)的緩存節(jié)點
const key = vnode.key
const cachedVNode = cache.get(key)

key是生成子節(jié)點的渲染函數(shù)時添加的,一般情況下就是0,1,2,...這些數(shù)字。

  • 記錄下點前的key
pendingCacheKey = key
  • 如果有找到緩存的cachedVNode節(jié)點,將緩存的cachedVNode節(jié)點的組件實例和節(jié)點元素 復(fù)制給新的VNode節(jié)點。沒有找到就先將當前子樹節(jié)點VNodependingCacheKey加入到Keys中。
if (cachedVNode) {
  // 復(fù)制節(jié)點
  vnode.el = cachedVNode.el
  vnode.component = cachedVNode.component
  // 標記 | ShapeFlags.COMPONENT_KEPT_ALIVE,這個組件是復(fù)用的`VNode`, 這個標記 不走 mount邏輯
  vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
} else {
  // 添加 pendingCacheKey
  keys.add(key)
}

問題: 這里為什么不實現(xiàn)在cache中存入{pendingCacheKey: vnode}呢?
答案: 這里其實可以加入這邏輯,只是官方間隔這個邏輯延后實現(xiàn)了, 我覺得沒什么差別。

  • 在組件掛載onMounted和更新onUpdated的時候添加/更新緩存
onMounted(cacheSubtree)
onUpdated(cacheSubtree)

const cacheSubtree = () => {
  if (pendingCacheKey != null) {
    // 添加/更新緩存
    cache.set(pendingCacheKey, instance.subTree)
  }
}

全部代碼
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,

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

    let current: VNode | null = null
    // 緩存的一些數(shù)據(jù)
    const cache = new Map()
    const keys: Keys = new Set()
    let pendingCacheKey: CacheKey | null = null

    // 更新/添加緩存數(shù)據(jù)
    const cacheSubtree = () => {
      if (pendingCacheKey != null) {
        // 添加/更新緩存
        cache.set(pendingCacheKey, instance.subTree)
      }
    }

    // 監(jiān)聽生命周期
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

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

      // 獲取緩存
      const key = rawVNode.key
      const cachedVNode = cache.get(key)

      pendingCacheKey = key

      if (cachedVNode) {
        // 復(fù)用DOM和組件實例
        rawVNode.el = cachedVNode.el
        rawVNode.component = cachedVNode.component
      } else {
        // 添加 pendingCacheKey
        keys.add(key)
      }

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

至此,通過cache實現(xiàn)了DOM組件實例的緩存。

keep-alivepatch復(fù)用邏輯

我們知道生成VNode后是進行patch邏輯,生成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
      )
    }
  }
}

processComponent處理組件邏輯的時候如果是復(fù)用ShapeFlags.COMPONENT_KEPT_ALIVE則走的父組件keep-aliveactivate方法;

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卸載的keep-alive組件ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE時調(diào)用父組件keep-alivedeactivate方法。

總結(jié):keep-alive組件的復(fù)用和卸載被activate方法和deactivate方法接管了。

active邏輯
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
  const instance = vnode.component!
  // 1. 直接掛載DOM
  move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
  // 2. 更新prop
  patch(
    instance.vnode,
    vnode,
    container,
    anchor,
    instance,
    parentSuspense,
    isSVG,
    vnode.slotScopeIds,
    optimized
  )
  // 3. 異步執(zhí)行onVnodeMounted 鉤子函數(shù)
  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. 直接掛載DOM
  2. 更新prop
  3. 異步執(zhí)行onVnodeMounted鉤子函數(shù)
deactivate邏輯
const storageContainer = createElement('div')

sharedContext.deactivate = (vnode: VNode) => {
  const instance = vnode.component!
  // 1. 把DOM移除,掛載在一個新建的div下
  move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
  // 2. 異步執(zhí)行onVnodeUnmounted鉤子函數(shù)
  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. DOM移除,掛載在一個新建的div
  2. 異步執(zhí)行onVnodeUnmounted鉤子函數(shù)

問題:舊節(jié)點的deactivate和新節(jié)點的active誰先執(zhí)行
答案:舊節(jié)點的deactivate先執(zhí)行,新節(jié)點的active后執(zhí)行。

keep-aliveunmount邏輯
  • cache中出當前子樹VNode節(jié)點外的所有卸載,當前組件取消keep-alive的標記, 這樣當前子樹VNode會隨著keep-alive的卸載而卸載。
onBeforeUnmount(() => {
  cache.forEach(cached => {
    const { subTree, suspense } = instance
    const vnode = getInnerChild(subTree)
    if (cached.type === vnode.type) {
      // 當然組件先取消`keep-alive`的標記,能正在執(zhí)行unmout
      resetShapeFlag(vnode)
      // but invoke its deactivated hook here
      const da = vnode.component!.da
      da && queuePostRenderEffect(da, suspense)
      return
    }
    // 每個緩存的VNode,執(zhí)行unmount方法
    unmount(cached)
  })
})

<!-- 執(zhí)行unmount -->
function unmount(vnode: VNode) {
    // 取消`keep-alive`的標記,能正在執(zhí)行unmout
    resetShapeFlag(vnode)
    // unmout
    _unmount(vnode, instance, parentSuspense)
}

keep-alive卸載了,其緩存的DOM也將被卸載。

keep-alive緩存的配置include,excludemax

這部分知道邏輯就好了,不做代碼分析。

  1. 組件名稱在include中的組件會被緩存;
  2. 組件名稱在exclude中的組件不會被緩存;
  3. 規(guī)定緩存的最大數(shù)量,如果超過了就把緩存的最前面的內(nèi)容刪除。

動態(tài)組件

使用方法
<keep-alive>
  <component is="A"></component>
</keep-alive>
渲染函數(shù)
resolveDynamicComponent("A")
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
}

指令一樣,resolveDynamicComponent就是根據(jù)名稱尋找局部或者全局注冊的組件,然后渲染對應(yīng)的組件。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容