一文聊聊Vue中的KeepAlive元件

2022-11-14 22:00:26

前端(vue)入門到精通課程:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:

最近看 Vue 相關的知識點,看到 KeepAlive 元件時比較好奇它是怎麼做到元件間切換時不重新渲染的,於是便稍微深入的瞭解了一下。(學習視訊分享:)

如果你也有興趣想要了解一下具體內部怎麼實現的或者說有一定的瞭解但是不夠熟悉,那麼正好你也可以一起鞏固下

Tips: 這樣面試的時候你就可以大聲的問別人這個知識點了?。

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>
登入後複製

可以看這個錄屏,在首次載入後再次頻繁的切換並沒有重新銷燬與掛載,而僅僅是將元件進行了失活(而不是銷燬),渲染時只需要重新啟用就可以,而不需重新掛載,如果要渲染的元件很大,那就能有不錯的效能優化。

想要體驗的話可以去看看這個例子?,其中資料會被快取這個也需要在開發使用中去注意到的

如何實現

實現原理其實很簡單,其實就是快取管理和特定的銷燬和渲染邏輯,使得它不同於其他元件。

KeepAlive 元件在解除安裝元件時並不能真的將其解除安裝,而是將其放到一個隱藏的容器裡面當被啟用時再從隱藏的容器中拿出來掛載到真正的 dom 上就行,這也就對應了 KeepAlive 的兩個獨特的生命週期activateddeactivated

image.png

先來簡單瞭解下元件的掛載過程

image.png所以在 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
  }
}
登入後複製
  • 如何掛載activateddeactivated生命週期(生命週期相關可以不用重點看)

首先這兩個生命週期是在 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.activatesharedContext.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如何實現的簡化流程圖

image.png

需要注意的知識點

1、什麼時候快取

KeepAlive 元件的onMountedonUpdated生命週期時進行快取

2、什麼時候取消快取

  • 快取數量超過設定的 max 時

  • 監聽 include 和 exclude 修改的時候,會讀取快取中的知進行判斷是否需要清除快取

修剪快取的時候也要 unmount(如果該快取不是當前元件)或者 resetShapeFlag 將標誌為從 KeepAlive 相關 shapeFlag 狀態重置為 STATEFUL_COMPONENT 狀態(如果該快取是當前元件,但是被exclude了),當然 unmount 函數內包含 resetShapeFlag 操作

3、快取策略

KeepAlive 元件的快取策略是 LRU(last recently used)快取策略

核心思想在於需要把當前存取或渲染的元件作為最新一次渲染的元件,並且該元件在快取修剪過程中始終是安全的,即不會被修剪。

看下面的圖更加直觀,

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元件的詳細內容,更多請關注TW511.COM其它相關文章!