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

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

例如最近我們遇到了一種場(chǎng)景匿辩,某個(gè)組件上傳較大的文件是個(gè)耗時(shí)的操作腰耙,如果上傳的時(shí)候切換到其他頁(yè)面內(nèi)容,組件會(huì)被卸載铲球,對(duì)應(yīng)的下載也會(huì)被取消挺庞。此時(shí)可以用keep-alive組件包裹這個(gè)組件,在切換到其他頁(yè)面時(shí)該組件仍然可以繼續(xù)上傳文件稼病,切換回來(lái)也可以看到上傳進(jìn)度选侨。

keep-alive

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

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

    // 需要渲染的子樹(shù)VNode
    let current: VNode | null = null

    return () => {

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

      // 標(biāo)記 | ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE然走,這個(gè)組件是`keep-alive`組件, 這個(gè)標(biāo)記 不走 unmount邏輯援制,因?yàn)橐痪彺娴?      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      // 記錄當(dāng)前子節(jié)點(diǎn)
      current = vnode

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

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

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

key是生成子節(jié)點(diǎn)的渲染函數(shù)時(shí)添加的,一般情況下就是0竟贯,1答捕,2,...這些數(shù)字屑那。

  • 記錄下點(diǎn)前的key
pendingCacheKey = key
  • 如果有找到緩存的cachedVNode節(jié)點(diǎn)拱镐,將緩存的cachedVNode節(jié)點(diǎn)的組件實(shí)例和節(jié)點(diǎn)元素 復(fù)制給新的VNode節(jié)點(diǎn)。沒(méi)有找到就先將當(dāng)前子樹(shù)節(jié)點(diǎn)VNodependingCacheKey加入到Keys中持际。
if (cachedVNode) {
  // 復(fù)制節(jié)點(diǎn)
  vnode.el = cachedVNode.el
  vnode.component = cachedVNode.component
  // 標(biāo)記 | ShapeFlags.COMPONENT_KEPT_ALIVE沃琅,這個(gè)組件是復(fù)用的`VNode`, 這個(gè)標(biāo)記 不走 mount邏輯
  vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
} else {
  // 添加 pendingCacheKey
  keys.add(key)
}

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

  • 在組件掛載onMounted和更新onUpdated的時(shí)候添加/更新緩存
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)聽(tīng)生命周期
    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和組件實(shí)例
        rawVNode.el = cachedVNode.el
        rawVNode.component = cachedVNode.component
      } else {
        // 添加 pendingCacheKey
        keys.add(key)
      }

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

至此岳枷,通過(guò)cache實(shí)現(xiàn)了DOM組件實(shí)例的緩存芒填。

keep-alivepatch復(fù)用邏輯

我們知道生成VNode后是進(jìn)行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處理組件邏輯的時(shí)候如果是復(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時(shí)調(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移除闷祥,掛載在一個(gè)新建的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移除娱颊,掛載在一個(gè)新建的div
  2. 異步執(zhí)行onVnodeUnmounted鉤子函數(shù)

問(wèn)題:舊節(jié)點(diǎn)的deactivate和新節(jié)點(diǎn)的active誰(shuí)先執(zhí)行
答案:舊節(jié)點(diǎn)的deactivate先執(zhí)行,新節(jié)點(diǎn)的active后執(zhí)行凯砍。

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

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

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

keep-alive緩存的配置include,excludemax

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

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

動(dòng)態(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ù)名稱(chēng)尋找局部或者全局注冊(cè)的組件来氧,然后渲染對(duì)應(yīng)的組件。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末香拉,一起剝皮案震驚了整個(gè)濱河市饲漾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌缕溉,老刑警劉巖考传,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異证鸥,居然都是意外死亡僚楞,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)枉层,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)泉褐,“玉大人,你說(shuō)我怎么就攤上這事鸟蜡∧ぴ撸” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵揉忘,是天一觀的道長(zhǎng)跳座。 經(jīng)常有香客問(wèn)我端铛,道長(zhǎng),這世上最難降的妖魔是什么疲眷? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任禾蚕,我火速辦了婚禮,結(jié)果婚禮上狂丝,老公的妹妹穿的比我還像新娘换淆。我一直安慰自己,他們只是感情好几颜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布倍试。 她就那樣靜靜地躺著,像睡著了一般蛋哭。 火紅的嫁衣襯著肌膚如雪易猫。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,598評(píng)論 1 305
  • 那天具壮,我揣著相機(jī)與錄音准颓,去河邊找鬼。 笑死棺妓,一個(gè)胖子當(dāng)著我的面吹牛攘已,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播怜跑,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼样勃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了性芬?” 一聲冷哼從身側(cè)響起峡眶,我...
    開(kāi)封第一講書(shū)人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎植锉,沒(méi)想到半個(gè)月后辫樱,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡俊庇,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年狮暑,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片辉饱。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡搬男,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出彭沼,到底是詐尸還是另有隱情缔逛,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站褐奴,受9級(jí)特大地震影響按脚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜歉糜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望望众。 院中可真熱鬧匪补,春花似錦、人聲如沸烂翰。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)甘耿。三九已至踊兜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間佳恬,已是汗流浹背捏境。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留毁葱,地道東北人垫言。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像倾剿,于是被迫代替她去往敵國(guó)和親筷频。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容