一文聊聊Vue中的KeepAlive組件
最近看 Vue 相關的知識點,看到 KeepAlive 元件時比較好奇它是怎麼做到元件間切換時不重新渲染的,於是便稍微深入的了解了一下。 (學習影片分享:vue影片教學)
如果你也有興趣想要了解一下具體內部怎麼實現的或者說有一定的了解但是不夠熟悉,那麼正好你也可以一起鞏固下
Tips: 這樣面試的時候你就可以大聲的問別人這個知識點了?。
KeepAlive 是什麼
<keepalive></keepalive>
是一個內建元件,它的功能是在多個元件間動態切換時快取移除的元件實例。
KeepAlive 功能
KeepAlive 一詞藉鑑於HTTP 協議,在HTTP 協議裡面KeepAlive 又稱持久連接,作用是允許多個請求/回應共用同一個HTTP連接,解決了頻繁的銷毀和建立HTTP 連接帶來的額外效能開銷。而同理 Vue 裡的 KeepAlive 元件也是為了避免一個元件被頻繁的銷毀/重建,避免了效能上的開銷。
// App.vue <Test :msg="curTab" v-if="curTab === 'Test'"></Test> <HelloWorld :msg="curTab" v-if="curTab === 'HelloWorld'"></HelloWorld> <div @click="toggle">toggle</div>
上述程式碼可以看到,如果我們頻繁點擊toggle 時會頻繁的渲染Test/HelloWorld 元件,當使用者頻繁的點擊時Test 元件需要頻繁的銷毀/渲染,這就造成很大的渲染性能損失。
所以為了解決這個效能開銷,你需要知道是時候使用 KeepAlive 元件。
<KeepAlive> <component :is="curTab === 'Test' ? Test : HelloWorld" :msg="curTab"></component> </KeepAlive> <div @click="toggle">toggle</div>
可以看這個錄屏,在首次加載後再次頻繁的切換並沒有重新銷毀與掛載,而僅僅是將組件進行了失活(而不是銷毀),渲染時只需要重新啟動就可以,而不需重新掛載,如果要渲染的元件很大,那就能有不錯的效能優化。
想要體驗的話可以去看看這個例子?官方demo,其中資料會被緩存這個也需要在開發使用中去注意到的
如何實作
實作原理其實很簡單,其實就是快取管理和特定的銷毀和渲染邏輯,使得它有別於其他元件。
KeepAlive 元件在卸載元件時並不能真的將其卸載,而是將其放到一個隱藏的容器裡面,當被啟動時再從隱藏的容器中拿出來掛載到真正的dom 上就行,這也就對應了KeepAlive 的兩個獨特的生命週期activated
和deactivated
。
先來簡單了解下元件的掛載過程
所以在KeepAlive 內的子元件在mount 和unmount 的時候會執行特定的渲染邏輯,從而不會去走掛載和銷毀邏輯
#具體實作(實作一個小而簡單的KeepAlive)
KeepAlive 元件的屬性
const KeepAliveImpl: ComponentOptions = { name: "KeepAlive", // 标识这是一个 KeepAlive 组件 __isKeepAlive: true, // props props: { exclude: [String, Array, RegExp], include: [String, Array, RegExp], max: [String, Number] } } // isKeepAlive export const isKeepAlive = (vnode: VNode): boolean => (vnode.type as any).__isKeepAlive
KeepAlive 元件的setup 邏輯以及渲染邏輯(重點看)
// setup 接着上面的代码 // 获取到当前 KeepAlive 组件实例 const instance = getCurrentInstance()! as any; // 拿到 ctx const sharedContext = instance.ctx as KeepAliveContext; // cache 缓存 // key: vnode.key | vnode.type value: vnode const cache: Cache = new Map() // 需要拿到某些的 renderer 操作函数,需要自己特定执行渲染和卸载逻辑 const { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContext // 隐藏的容器,用来存储需要隐藏的 dom const storeageContainer = createElement('div') // 存储当前的子组件的缓存 key let pendingKey: CacheKey | null = null sharedContext.activate = (vnode, container, anchor) => { // KeepAlive 下组件激活时执行的 move 逻辑 move(vnode, container, anchor, 0 /* ENTER */) } sharedContext.deactivate = (vnode) => { // KeepAlive 下组件失活时执行的 move 逻辑 move(vnode, storeageContainer, null, 1 /* LEAVE */) } return () => { // 没有子组件 if (!slots.default) { return null; } const children = slots.default() as VNode[]; const rawNode = children[0]; let vnode = rawNode; const comp = vnode.type as ConcreteComponent; const name = comp.displayName || comp.name const { include, exclude } = props; // 没有命中的情况 if ( (include && (!name || !matches(include, name))) || (exclude && name && matches(exclude, name)) ) { // 直接渲染子组件 return rawNode; } // 获取子组件的 vnode key const key = vnode.key == null ? comp : vnode.key; // 获取子组件缓存的 vnode const cachedVNode = cache.get(key); pendingKey = key; // 命中缓存 if (cachedVNode) { vnode.el = cachedVNode.el; // 继承组件实例 vnode.component = cachedVNode.component; // 在 vnode 上更新 shapeFlag,标记为 COMPONENT_KEPT_ALIVE 属性,防止渲染器重新挂载 vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE } else { // 没命中将其缓存 cache.set(pendingKey, vnode) } // 在 vnode 上更新 shapeFlag,标记为 COMPONENT_SHOULD_KEEP_ALIVE 属性,防止渲染器将组件卸载了 vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE // 渲染组件 vnode return vnode; }
KeepAlive元件mount 時掛載renderer 到ctx 上
在KeepAlive 元件內會從sharedContext 上的renderer 拿到一些方法例如move、createElement 等
function mountComponent() { // ... if (isKeepAlive(initialVNode)) { ;(instance.ctx as KeepAliveContext).renderer = internals } }
子元件執行特定的銷毀和渲染邏輯
首先從上面可以看到,在渲染KeepAlive 元件時會對其子元件的vnode 上增加對應的shapeFlag 標誌
例如COMPONENT_KEPT_ALIVE
標誌,元件掛載的時候告訴渲染器這個不需要mount而需要特殊處理
const processComponent = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, ) => { if (n1 == null) { // 在 KeepAlive 组件渲染时会对子组件增加 COMPONENT_KEPT_ALIVE 标志 // 挂载子组件时会判断是否 COMPONENT_KEPT_ALIVE ,如果是不会调用 mountComponent 而是直接执行 activate 方法 if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).activate( n2, container, anchor ) } // ... } }
同理COMPONENT_SHOULD_KEEP_ALIVE
標誌也是用來在元件卸載的時候告訴渲染器這個不需要unmount 而需要特殊處理。
const unmount: UnmountFn = (vnode) => { // ... // 在 KeepAlive 组件渲染时会对子组件增加 COMPONENT_SHOULD_KEEP_ALIVE 标志 // 然后在子组件卸载时并不会真实的卸载而是调用 KeepAlive 的 deactivate 方法 if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) { ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode) return } }
如何掛載
activated
和deactivated
生命週期(生命週期相關可以不用重點看)
#首先這兩個生命週期是在KeepAlive 元件內獨特宣告的,是直接匯出使用的。
export function onActivated( hook: Function, target?: ComponentInternalInstance | null ) { // 注册 activated 的回调函数到当前的 instance 的钩子函数上 registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target) } export function onDeactivated( hook: Function, target?: ComponentInternalInstance | null ) { // 注册 deactivated 的回调函数到当前的 instance 的钩子函数上 registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target) }
然后因为这两个生命周期会注册在 setup 里面,所以只要执行 setup 就会将两个生命周期的回调函数注册到当前的 instance 实例上
// renderer.ts // mount 函数逻辑 const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => { // ... const instance: ComponentInternalInstance = compatMountInstance || (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense )) // 执行 setup setupComponent(instance) } // setupcomponent 处理 setup 函数值 export function setupComponent( instance: ComponentInternalInstance, isSSR = false ) { // ... const isStateful = isStatefulComponent(instance) // ... const setupResult = isStateful // setupStatefulComponent 函数主要功能是设置当前的 instance ? setupStatefulComponent(instance, isSSR) : undefined // ... } function setupStatefulComponent( instance: ComponentInternalInstance ){ if (setup) { //设置当前实例 setCurrentInstance(instance) // 执行组件内 setup 函数,执行 onActivated 钩子函数进行回调函数收集 const setupResult = callWithErrorHandling( setup, instance, ErrorCodes.SETUP_FUNCTION, [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext] ) // currentInstance = null; unsetCurrentInstance() } }
最后在执行sharedContext.activate
和sharedContext.deactivate
的时候将注册在实例上的回调函数取出来直接执行就OK了,执行时机在 postRender 之后
sharedContext.activate = (vnode, container, anchor) => { // KeepAlive 下组件激活时执行的 move 逻辑 move(vnode, container, anchor, 0 /* ENTER */) // 把回调推入到 postFlush 的异步任务队列中去执行 queuePostRenderEffect(() => { if (instance.a) { // a是 activated 钩子的简称 invokeArrayFns(instance.a) } }) } sharedContext.activate = (vnode, container, anchor) => { // KeepAlive 下组件失活时执行的 move 逻辑 move(vnode, container, anchor, 0 /* ENTER */) queuePostRenderEffect(() => { if (instance.da) { // da是 deactivated 钩子的简称 invokeArrayFns(instance.da) } }) } export const enum LifecycleHooks { // ... 其他生命周期声明 DEACTIVATED = 'da', ACTIVATED = 'a', } export interface ComponentInternalInstance { // ... 其他生命周期 [LifecycleHooks.ACTIVATED]: Function[] [LifecycleHooks.DEACTIVATED]: Function[] }
以下是关于上述demo如何实现的简化流程图
需要注意的知识点
1、什么时候缓存
KeepAlive 组件的onMounted
和onUpdated
生命周期时进行缓存
2、什么时候取消缓存
缓存数量超过设置的 max 时
- 监听 include 和 exclude 修改的时候,会读取缓存中的知进行判断是否需要清除缓存
修剪缓存的时候也要 unmount(如果该缓存不是当前组件)或者 resetShapeFlag 将标志为从 KeepAlive 相关 shapeFlag 状态重置为 STATEFUL_COMPONENT 状态(如果该缓存是当前组件,但是被exclude了),当然 unmount 函数内包含 resetShapeFlag 操作
3、缓存策略
KeepAlive 组件的缓存策略是 LRU(last recently used)缓存策略
核心思想在于需要把当前访问或渲染的组件作为最新一次渲染的组件,并且该组件在缓存修剪过程中始终是安全的,即不会被修剪。
看下面的图更加直观,图片来源一篇讲keepAlive 缓存优化的文章
4、如何添加到 vue devtools 组件树上
sharedContext.activate = (vnode, container, anchor) => { // instance 是子组件实例 const instance = vnode.component! // ... // dev环境下设置, 自己模拟写的 devtools.emit('component:added', instance.appContext.app, instance.uid, instance.parent ? instance.parent.uid: undefined, instance) // 官方添加 if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) { // Update components tree devtoolsComponentAdded(instance) } } // 同理 sharedContext.deactivates 上也要添加,不然不会显示在组件树上
5、缓存的子组件 props 更新处理
当子组件有 prop 更新时是需要重新去 patch 的,所以在 activate 的时候需要重新执行 patch 进行子组件更新
sharedContext.activate = (vnode, container, anchor) => { // ... // props 改变需要重新 patch(update) patch( instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, vnode.slotScopeIds, optimized ) }
以上是一文聊聊Vue中的KeepAlive組件的詳細內容。更多資訊請關注PHP中文網其他相關文章!

熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

記事本++7.3.1
好用且免費的程式碼編輯器

SublimeText3漢化版
中文版,非常好用

禪工作室 13.0.1
強大的PHP整合開發環境

Dreamweaver CS6
視覺化網頁開發工具

SublimeText3 Mac版
神級程式碼編輯軟體(SublimeText3)

熱門話題

PHP與Vue:完美搭檔的前端開發利器在當今網路快速發展的時代,前端開發變得愈發重要。隨著使用者對網站和應用的體驗要求越來越高,前端開發人員需要使用更有效率和靈活的工具來創建響應式和互動式的介面。 PHP和Vue.js作為前端開發領域的兩個重要技術,搭配起來可以稱得上是完美的利器。本文將探討PHP和Vue的結合,以及詳細的程式碼範例,幫助讀者更好地理解和應用這兩

在前端開發面試中,常見問題涵蓋廣泛,包括HTML/CSS基礎、JavaScript基礎、框架和函式庫、專案經驗、演算法和資料結構、效能最佳化、跨域請求、前端工程化、設計模式以及新技術和趨勢。面試官的問題旨在評估候選人的技術技能、專案經驗以及對行業趨勢的理解。因此,應試者應充分準備這些方面,以展現自己的能力和專業知識。

Django是一個由Python編寫的web應用框架,它強調快速開發和乾淨方法。儘管Django是web框架,但要回答Django是前端還是後端這個問題,需要深入理解前後端的概念。前端是指使用者直接和互動的介面,後端是指伺服器端的程序,他們透過HTTP協定進行資料的互動。在前端和後端分離的情況下,前後端程式可以獨立開發,分別實現業務邏輯和互動效果,資料的交

身為C#開發者,我們的開發工作通常包括前端和後端的開發,而隨著技術的發展和專案的複雜性提高,前端與後端協同開發也變得越來越重要和複雜。本文將分享一些前端與後端協同開發的技巧,以幫助C#開發者更有效率地完成開發工作。確定好介面規範前後端的協同開發離不開API介面的交互。要確保前後端協同開發順利進行,最重要的是定義好介面規格。接口規範涉及到接口的命

Go語言作為一種快速、高效的程式語言,在後端開發領域廣受歡迎。然而,很少有人將Go語言與前端開發聯繫起來。事實上,使用Go語言進行前端開發不僅可以提高效率,還能為開發者帶來全新的視野。本文將探討使用Go語言進行前端開發的可能性,並提供具體的程式碼範例,幫助讀者更了解這一領域。在傳統的前端開發中,通常會使用JavaScript、HTML和CSS來建立使用者介面

實作即時通訊的方法有WebSocket、Long Polling、Server-Sent Events、WebRTC等等。詳細介紹:1、WebSocket,它可以在客戶端和伺服器之間建立持久連接,實現即時的雙向通信,前端可以使用WebSocket API來創建WebSocket連接,並透過發送和接收訊息來實現即時通訊;2、Long Polling,是一種模擬即時通訊的技術等等

Golang與前端技術結合:探討Golang如何在前端領域發揮作用,需要具體程式碼範例隨著互聯網和行動應用的快速發展,前端技術也愈發重要。而在這個領域中,Golang作為一門強大的後端程式語言,也可以發揮重要作用。本文將探討Golang如何與前端技術結合,以及透過具體的程式碼範例來展示其在前端領域的潛力。 Golang在前端領域的角色作為一門高效、簡潔且易於學習的

Django:前端和後端開發都能搞定的神奇框架! Django是一個高效、可擴展的網路應用程式框架。它能夠支援多種Web開發模式,包括MVC和MTV,可以輕鬆地開發出高品質的Web應用程式。 Django不僅支援後端開發,還能夠快速建構出前端的介面,透過模板語言,實現靈活的視圖展示。 Django把前端開發和後端開發融合成了一種無縫的整合,讓開發人員不必專門學習
