解讀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)VNode的key, 緩存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)VNode的pendingCacheKey加入到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-alive的patch復(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-alive的activate方法;
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-alive的deactivate方法。
總結(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) }
- 直接掛載DOM
- 更新prop
- 異步執(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) ? } }
- 把DOM移除,掛載在一個(gè)新建的div下
- 異步執(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-alive的unmount邏輯
將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,exclude和max
這部分知道邏輯就好了,不做代碼分析。
- 組件名稱在include中的組件會(huì)被緩存;
- 組件名稱在exclude中的組件不會(huì)被緩存;
- 規(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ù)名稱尋找局部或者全局注冊(cè)的組件,然后渲染對(duì)應(yīng)的組件。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
詳解如何實(shí)現(xiàn)Vue組件的動(dòng)態(tài)綁定
Vue.js 是一個(gè)漸進(jìn)式框架,用于構(gòu)建用戶界面,在開(kāi)發(fā)過(guò)程中,我們經(jīng)常需要根據(jù)不同的條件動(dòng)態(tài)顯示組件,在本文中,我將詳細(xì)介紹如何實(shí)現(xiàn)Vue組件的動(dòng)態(tài)綁定,提供示例代碼,以幫助你更深入地理解這個(gè)概念,需要的朋友可以參考下2024-11-11vue el-switch綁定數(shù)值時(shí)需要注意的問(wèn)題
在Vue中使用`el-switch`組件時(shí),綁定數(shù)值類型時(shí)應(yīng)使用布爾值(true/false),而綁定字符串類型時(shí)應(yīng)使用字符串('true'/'false')2024-12-12Vue實(shí)現(xiàn)點(diǎn)擊時(shí)間獲取時(shí)間段查詢功能
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)點(diǎn)擊時(shí)間獲取時(shí)間段查詢功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04vue watch監(jiān)控對(duì)象的簡(jiǎn)單方法示例
這篇文章主要給大家介紹了關(guān)于vue watch監(jiān)控對(duì)象的簡(jiǎn)單方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01解決webpack+Vue引入iView找不到字體文件的問(wèn)題
今天小編就為大家分享一篇解決webpack+Vue引入iView找不到字體文件的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09記一次Vue.js混入mixin的使用(分權(quán)限管理頁(yè)面)
這篇文章主要介紹了記一次Vue.js混入mixin的使用(分權(quán)限管理頁(yè)面),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-04-04vue3數(shù)據(jù)更新而視圖未更新問(wèn)題解決
本文主要介紹了vue3?解決數(shù)據(jù)更新而視圖未更新問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-10-10