首頁  >  文章  >  web前端  >  一文聊聊Vue中的KeepAlive組件

一文聊聊Vue中的KeepAlive組件

青灯夜游
青灯夜游轉載
2022-11-14 20:21:131838瀏覽

一文聊聊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 === &#39;Test&#39;"></Test>
<HelloWorld :msg="curTab" v-if="curTab === &#39;HelloWorld&#39;"></HelloWorld>
<div @click="toggle">toggle</div>

上述程式碼可以看到,如果我們頻繁點擊toggle 時會頻繁的渲染Test/HelloWorld 元件,當使用者頻繁的點擊時Test 元件需要頻繁的銷毀/渲染,這就造成很大的渲染性能損失。

所以為了解決這個效能開銷,你需要知道是時候使用 KeepAlive 元件。

<KeepAlive>
  <component :is="curTab === &#39;Test&#39; ? Test : HelloWorld" :msg="curTab"></component>
</KeepAlive>
<div @click="toggle">toggle</div>

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

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

如何實作

實作原理其實很簡單,其實就是快取管理和特定的銷毀和渲染邏輯,使得它有別於其他元件。

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

一文聊聊Vue中的KeepAlive組件

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

一文聊聊Vue中的KeepAlive組件所以在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(&#39;div&#39;)

// 存储当前的子组件的缓存 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 = &#39;da&#39;,
  ACTIVATED = &#39;a&#39;,
}
export interface ComponentInternalInstance {
// ... 其他生命周期
[LifecycleHooks.ACTIVATED]: Function[]
[LifecycleHooks.DEACTIVATED]: Function[]
}

以下是关于上述demo如何实现的简化流程图

一文聊聊Vue中的KeepAlive組件

需要注意的知识点

1、什么时候缓存

KeepAlive 组件的onMountedonUpdated生命周期时进行缓存

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(&#39;component:added&#39;, 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
  )
}

(学习视频分享:web前端开发编程基础视频

以上是一文聊聊Vue中的KeepAlive組件的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除