Vue3核心源碼解析 (五) : 內(nèi)置組件<keep-alive>

??<keep-alive>是Vue.js的一個(gè)內(nèi)置組件,可以使被包含的組件保留狀態(tài)或避免重新渲染。下面來分析源碼runtime-core/src/components/KeepAlive.ts的實(shí)現(xiàn)原理。
??在setup方法中會(huì)創(chuàng)建一個(gè)緩存容器和緩存的key列表,其代碼如下:

  setup(props: KeepAliveProps, { slots }: SetupContext) {
// keep-alive組件的上下文對(duì)象
    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
      }
    }
/* 緩存對(duì)象 */
    const cache: Cache = new Map()
    const keys: Keys = new Set()
       // 替換內(nèi)容
       sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
         const instance = vnode.component!
         move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
         // 處理props改變
         patch(
         ...
         )
         ...
       }
       // 替換內(nèi)容
       sharedContext.deactivate = (vnode: VNode) => {
         const instance = vnode.component!
         move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
         ...
       }
     }

??<keep-alive>自己實(shí)現(xiàn)了render方法,并沒有使用Vue內(nèi)置的render方法(經(jīng)過<template>內(nèi)容提取、轉(zhuǎn)換AST、render字符串等一系列過程),在執(zhí)行<keep-alive>組件渲染時(shí),就會(huì)執(zhí)行這個(gè)render方法:

    return () => {
      pendingCacheKey = null

      if (!slots.default) {
        return null
      }
// 得到插槽中的第一個(gè)組件
      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
 // 獲取組件名稱,優(yōu)先獲取組件的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
 // 如果已經(jīng)緩存了,則直接從緩存中獲取組件實(shí)例給vnode,若還未緩存,則先進(jìn)行緩存
      if (cachedVNode) {
        // copy over mounted state
        vnode.el = cachedVNode.el
        vnode.component = cachedVNode.component
 // 執(zhí)行transition
        if (vnode.transition) {
          // recursively update transition hooks on subTree
          setTransitionHooks(vnode, vnode.transition!)
        }
//  設(shè)置shapeFlag標(biāo)志位,為了避免執(zhí)行組件mounted方法
        // avoid vnode being mounted as fresh
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        // make this key the freshest
   // 重新設(shè)置一下key保證最新
        keys.delete(key)
        keys.add(key)
      } else {
        keys.add(key)
        // prune oldest entry
  // 當(dāng)超出max值時(shí),清除緩存
        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
    }

??在上面的代碼中,當(dāng)緩存的個(gè)數(shù)超過max(默認(rèn)值為10)的值時(shí),就會(huì)清除舊的數(shù)據(jù),這其中就包含<keep-alive>的緩存更新策略,其遵循了LRU(Least Rencently Used)算法。

1. LRU算法

??LRU算法根據(jù)數(shù)據(jù)的歷史訪問記錄來淘汰數(shù)據(jù),其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的概率也更高”。利用這個(gè)思路,我們可以對(duì)<keep-alive>中緩存的組件數(shù)據(jù)進(jìn)行刪除和更新,其算法的核心實(shí)現(xiàn)如下:


LRU

上面的代碼中,主要利用map來存儲(chǔ)緩存數(shù)據(jù),利用map.keyIterator.next()來找到最久沒有使用的key對(duì)應(yīng)的數(shù)據(jù),從而對(duì)緩存進(jìn)行刪除和更新。

2. 緩存VNode對(duì)象

??在render方法中,<keep-alive>并不是直接緩存的DOM節(jié)點(diǎn),而是Vue中內(nèi)置的VNode對(duì)象,VNode經(jīng)過render方法后,會(huì)被替換成真正的DOM內(nèi)容。首先通過slots.default().children[0]獲取第一個(gè)子組件,獲取該組件的name。接下來會(huì)將這個(gè)name通過include與exclude屬性進(jìn)行匹配,若匹配不成功(說明不需要進(jìn)行緩存),則不進(jìn)行任何操作直接返回VNode。需要注意的是,<keep-alive>只會(huì)處理它的第一個(gè)子組件,所以如果給<keep-alive>設(shè)置多個(gè)子組件,是無法生效的。
??<keep-alive>還有一個(gè)watch方法,用來監(jiān)聽include和exclude的改變,代碼如下:

    // prune cache on include/exclude prop change
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {  // 監(jiān)聽include和exclude,在被修改時(shí)對(duì)cache進(jìn)行修正
        include && pruneCache(name => matches(include, name))
        exclude && pruneCache(name => !matches(exclude, name))
      },
      // prune post-render after `current` has been updated
      { flush: 'post', deep: true }
    )

??這里的程序邏輯是動(dòng)態(tài)監(jiān)聽include和exclude的改變,從而動(dòng)態(tài)地維護(hù)之前創(chuàng)建的緩存對(duì)象cache,其實(shí)就是對(duì)cache進(jìn)行遍歷,發(fā)現(xiàn)緩存的節(jié)點(diǎn)名稱和新的規(guī)則沒有匹配上時(shí),就把這個(gè)緩存節(jié)點(diǎn)從緩存中摘除。下面來看pruneCache這個(gè)方法,代碼如下:

    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中的所有項(xiàng),如果不符合filter指定的規(guī)則,則會(huì)執(zhí)行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對(duì)應(yīng)的組件實(shí)例
      cache.delete(key)
      keys.delete(key)
    }

上面的內(nèi)容完成以后,當(dāng)響應(yīng)式觸發(fā)時(shí),<keep-alive>中的內(nèi)容會(huì)改變,會(huì)調(diào)用<keep-alive>的render方法得到VNode,這里并沒有用很深層次的diff去對(duì)比緩存前后的VNode,而是直接將舊節(jié)點(diǎn)置為null,用新節(jié)點(diǎn)進(jìn)行替換,在patch方法中,直接命中這里的邏輯,代碼如下:

     // n1為緩存前的節(jié)點(diǎn),n2為將要替換的節(jié)點(diǎn)
     if (n1 && !isSameVNodeType(n1, n2)) {
       anchor = getNextHostNode(n1)
       // 卸載舊節(jié)點(diǎn)
       unmount(n1, parentComponent, parentSuspense, true)
       n1 = null
     }

然后通過setup方法中的sharedContext.activate和sharedContext.deactivate來進(jìn)行內(nèi)容的替換,其核心是move方法,代碼如下:

    const move: MoveFn = () => {
        // 替換DOM
        ...
        hostInsert(el!, container, anchor) // insertBefore修改DOM
     }

總結(jié)一下,<keep-alive>組件也是一個(gè)Vue組件,它通過自定義的render方法實(shí)現(xiàn),并且使用了插槽。由于是直接使用VNode方式進(jìn)行內(nèi)容替換,不是直接存儲(chǔ)DOM結(jié)構(gòu),因此不會(huì)執(zhí)行組件內(nèi)的生命周期方法,它通過include和exclude維護(hù)組件的cache對(duì)象,從而來處理緩存中的具體邏輯。

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

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

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