博客 / 詳情

返回

徹底弄懂KeepAlive

🧑‍💻 寫在開頭

點贊 + 收藏 === 學會🤣🤣🤣

前言

開發過Vue應用的同學對KeepAlive功能應該都不陌生了,但是大家對它的理解是隻停留在知道怎麼用的階段 還是説清晰的知道它內部的實現細節呢,在項目中因KeepAlive導致的的Bug能第一時間分析出來原因並且找到解決方法呢。這篇文章的目的就是想結合Vue渲染的核心細節來重新認識一下KeepAlive這個功能。

文章是基於Vue3.5.24版本做的分析

接下來我將通過對幾個問題的解釋,來慢慢梳理KeepAlive的細節。

帶着問題弄懂KeepAlive

1.編寫的Vue文件在瀏覽器運行時是什麼樣子的?

看一個下面的簡單例子

<template>
  <div>{{ count }}</div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const count = ref(0)
</script>

我們寫的這麼簡單的一段代碼,在運行前會被編譯成下面這個樣子,傳送門Vue SFC Playground

import { defineComponent as _defineComponent } from 'vue'
import { ref } from 'vue'


const __sfc__ = /*@__PURE__*/_defineComponent({
  __name: 'App',
  setup(__props, { expose: __expose }) {
  __expose();

const count = ref(0)

const __returned__ = { count }
Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
return __returned__
}

});
import { toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", null, _toDisplayString($setup.count), 1 /* TEXT */))
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__

通過結果可以看到,Vue文件中的內容編譯後變成了一個普通的JS對象。
其中主要包含以下幾個屬性
_name: 組件的名稱,未明確定義組件名稱的情況會使用文件的名稱作為組件的名稱。
setup: 組件中定義的setup函數,默認返回了定義的響應式數據。
render: 渲染函數,通過將template模板編譯而來,返回值是一個VNode。
_file: 組件的源文件名稱。

2.組件需要滿足什麼條件才會被緩存,緩存的是什麼?

想要回答好這個問題,就需要結合KeepAlive組件的源碼。

const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,

  __isKeepAlive: true,

  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number],
  },

  setup(props: KeepAliveProps, { slots }: SetupContext) {
    const instance = getCurrentInstance()!
 
    const sharedContext = instance.ctx as KeepAliveContext

    if (__SSR__ && !sharedContext.renderer) {
      return () => {
        const children = slots.default && slots.default()
        return children && children.length === 1 ? children[0] : children
      }
    }

    // key -> vNode  Map結構
    const cache: Cache = new Map()
    // 所有緩存的Key,保證當緩存數量超過max指定的值後,準確的移除最早緩存的實例
    const keys: Keys = new Set()
    // 當前渲染的vNode
    let current: VNode | null = null

    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      ;(instance as any).__v_cache = cache
    }

    const parentSuspense = instance.suspense

    const {
      renderer: {
        p: patch,
        m: move,
        um: _unmount,
        o: { createElement },
      },
    } = sharedContext
    const storageContainer = createElement('div')

    sharedContext.activate = (
      vnode,
      container,
      anchor,
      namespace,
      optimized,
    ) => {
      const instance = vnode.component!
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // in case props have changed
      patch(
        instance.vnode,
        vnode,
        container,
        anchor,
        instance,
        parentSuspense,
        namespace,
        vnode.slotScopeIds,
        optimized,
      )
      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)

      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        // Update components tree
        devtoolsComponentAdded(instance)
      }
    }

    sharedContext.deactivate = (vnode: VNode) => {
      const instance = vnode.component!
      invalidateMount(instance.m)
      invalidateMount(instance.a)

      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      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)
      }

      // for e2e test
      if (__DEV__ && __BROWSER__) {
        ;(instance as any).__keepAliveStorageContainer = storageContainer
      }
    }

    function unmount(vnode: VNode) {
      // reset the shapeFlag so it can be properly unmounted
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense, true)
    }

    function pruneCache(filter: (name: string) => boolean) {
      cache.forEach((vnode, key) => {
        const name = getComponentName(vnode.type as ConcreteComponent)
        if (name && !filter(name)) {
          pruneCacheEntry(key)
        }
      })
    }

    // 根據key移除緩存的實例
    function pruneCacheEntry(key: CacheKey) {
      const cached = cache.get(key) as VNode
      if (cached && (!current || !isSameVNodeType(cached, current))) {
        unmount(cached)
      } else if (current) {
        resetShapeFlag(current)
      }
      cache.delete(key)
      keys.delete(key)
    }

    // prune cache on include/exclude prop change
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        include && pruneCache(name => matches(include, name))
        exclude && pruneCache(name => !matches(exclude, name))
      },
      // prune post-render after `current` has been updated
      { flush: 'post', deep: true },
    )

    // 渲染結束後 緩存當前實例
    let pendingCacheKey: CacheKey | null = null
    const cacheSubtree = () => {
      if (pendingCacheKey != null) {
        if (isSuspense(instance.subTree.type)) {
          queuePostRenderEffect(() => {
            cache.set(pendingCacheKey!, getInnerChild(instance.subTree))
          }, instance.subTree.suspense)
        } else {
          cache.set(pendingCacheKey, getInnerChild(instance.subTree))
        }
      }
    }
    
    // 在這Mounted和Updated鈎子裏面 緩存當前渲染的實例
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

    onBeforeUnmount(() => {
      cache.forEach(cached => {
        const { subTree, suspense } = instance
        const vnode = getInnerChild(subTree)
        if (cached.type === vnode.type && cached.key === vnode.key) {
          resetShapeFlag(vnode)
          const da = vnode.component!.da
          da && queuePostRenderEffect(da, suspense)
          return
        }
        unmount(cached)
      })
    })

    // setup返回的是一個函數,這個函數會被直接當做組件的渲染函數
    return () => {
      pendingCacheKey = null

      if (!slots.default) {
        // 無子節點
        return (current = null)
      }

      const children = slots.default()
      const rawVNode = children[0]
      if (children.length > 1) {
        // 只能有一個子節點
        if (__DEV__) {
          warn(`KeepAlive should contain exactly one component child.`)
        }
        current = null
        return children
      } else if (
        !isVNode(rawVNode) ||
        (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
          !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
      ) {
        // 子節點必須是一個組件
        current = null
        return rawVNode
      }

      let vnode = getInnerChild(rawVNode)
     
      if (vnode.type === Comment) {
        current = null
        return vnode
      }

      const comp = vnode.type as ConcreteComponent

      // 獲得組件的名稱
      const name = getComponentName(
        isAsyncWrapper(vnode)
          ? (vnode.type as ComponentOptions).__asyncResolved || {}
          : comp,
      )

      const { include, exclude, max } = props
     
      // 根據include和exclude 判斷當前的組件是否需要緩存
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        // #11717
        vnode.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
        current = vnode
        return rawVNode
      }
      
      // 組件的key
      const key = vnode.key == null ? comp : vnode.key
      // 根據key 獲得已緩存的VNode
      const cachedVNode = cache.get(key)

      // clone vnode if it's reused because we are going to mutate it
      if (vnode.el) {
        vnode = cloneVNode(vnode)
        if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
          rawVNode.ssContent = vnode
        }
      }

      pendingCacheKey = key

      if (cachedVNode) {
        // 如果實例已被緩存
        // 複製DOM節點、組件實例
        vnode.el = cachedVNode.el
        vnode.component = cachedVNode.component
        if (vnode.transition) {
          // recursively update transition hooks on subTree
          setTransitionHooks(vnode, vnode.transition!)
        }
        // 標記組件是從緩存中恢復  防止組件被重新mounted
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        // 把緩存的key移動到隊尾
        keys.delete(key)
        keys.add(key)
      } else {
        // 實例未被緩存
        keys.add(key)
        // 如果換成數量超過max,刪除最早進入的實例
        if (max && keys.size > parseInt(max as string, 10)) {
          pruneCacheEntry(keys.values().next().value!)
        }
      }
      // 標記組件應該被緩存 防止組件被卸載
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
 
      current = vnode
      // 返回子節點的vNode
      return isSuspense(rawVNode.type) ? rawVNode : vnode
    }
  },
}

通過源碼的分析,可以回答上面的問題了 首先,KeepAlive的用法要符合它的規範,只能用它嵌套組件,並且只能嵌套一個子組件。 其次,如果設置了includeexclude限制,那麼組件的名稱必須要滿足這些限制才會被緩存,且當前KeepAlive緩存的數量未超過max的限制。 KeepAlive的渲染函數最終渲染的是它默認插槽的內容,緩存的是組件的VNode。

3.組件切換如何觸發所有子組件註冊的onActivated和onDeactivated函數的

想要回答好這個問題,也需要結合onActivatedonDeactivated函數的定義

export enum LifecycleHooks {
  BEFORE_CREATE = 'bc',
  CREATED = 'c',
  BEFORE_MOUNT = 'bm',
  MOUNTED = 'm',
  BEFORE_UPDATE = 'bu',
  UPDATED = 'u',
  BEFORE_UNMOUNT = 'bum',
  UNMOUNTED = 'um',
  // 組件實例上的da屬性,代表是該組件註冊的Deactivated函數
  DEACTIVATED = 'da',
  // 組件實例上的a屬性,代表是該組件註冊的Activated函數
  ACTIVATED = 'a',
  RENDER_TRIGGERED = 'rtg',
  RENDER_TRACKED = 'rtc',
  ERROR_CAPTURED = 'ec',
  SERVER_PREFETCH = 'sp',
}

// 註冊一個回調函數,若組件實例是<KeepAlive>緩存樹的一部分,當組件被插入到 DOM 中時調用
export function onActivated(
  hook: Function,
  target?: ComponentInternalInstance | null,
): void {
  registerKeepAliveHook(hook, LifecycleHooks.ACTIVATED, target)
}

// 註冊一個回調函數,若組件實例是<KeepAlive>緩存樹的一部分,當組件從 DOM 中被移除時調用。
export function onDeactivated(
  hook: Function,
  target?: ComponentInternalInstance | null,
): void {
  registerKeepAliveHook(hook, LifecycleHooks.DEACTIVATED, target)
}

function registerKeepAliveHook(
  hook: Function & { __wdc?: Function },
  type: LifecycleHooks,
  target: ComponentInternalInstance | null = currentInstance,
) {
   "__wdc" 代表 "with deactivation check(帶有失活檢查)".
  const wrappedHook =
    hook.__wdc ||
    (hook.__wdc = () => {
      // 僅當目標實例不在失活分支中時,才觸發掛鈎.
      let current: ComponentInternalInstance | null = target
      while (current) {
        if (current.isDeactivated) {
          return
        }
        current = current.parent
      }
      return hook()
    })
  // 在當前組件實例上註冊該回調函數
  injectHook(type, wrappedHook, target)
  // 把回調註冊到KeepAlive根組件上
  // 避免了在調用這些鈎子時遍歷整個組件樹的需要
  if (target) {
    let current = target.parent
    while (current && current.parent) {
      if (isKeepAlive(current.parent.vnode)) {
        injectToKeepAliveRoot(wrappedHook, type, target, current)
      }
      current = current.parent
    }
  }
}

function injectToKeepAliveRoot(
  hook: Function & { __weh?: Function },
  type: LifecycleHooks,
  target: ComponentInternalInstance,
  keepAliveRoot: ComponentInternalInstance,
) {
  // 將鈎子註冊到KeepAlive根組件上, 註冊到隊頭,優先與父組件同類型的鈎子觸發
  const injected = injectHook(type, hook, keepAliveRoot, true /* prepend */)
  // 當前組件卸載時,移除註冊的鈎子
  onUnmounted(() => {
    remove(keepAliveRoot[type]!, injected)
  }, target)
}

// 註冊鈎子,所有的鈎子包括onMounted在內的鈎子最終都是通過這個鈎子註冊的
export function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false,
): Function | undefined {
  if (target) {
    const hooks = target[type] || (target[type] = [])
    // “__weh”代表“with error handling(帶有錯誤檢查)”。
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) => {
        pauseTracking()
        const reset = setCurrentInstance(target)
        const res = callWithAsyncErrorHandling(hook, target, type, args)
        reset()
        resetTracking()
        return res
      })
    if (prepend) {
      // 插入到隊頭
      hooks.unshift(wrappedHook)
    } else {
      // 插入隊尾
      hooks.push(wrappedHook)
    }
    // 返回最終插入的鈎子函數
    return wrappedHook
  } 
}

看了函數的定義後,可以回答上面的問題了 通過onActivatedonDeactivated註冊的函數最終都會註冊到KeepAlive根組件的實例的ada屬性上,這兩個屬性都是數組,並且子組件對應的函數會註冊到KeepAlive根組件實例的ada屬性的隊頭,優先於父組件註冊的同類型的鈎子執行。等KeepAlive根組件切換時,只需要按需調用根組件實例上的ada中所有的函數即可。

4.KeepAlive是如何處理組件的移除和恢復的

4.1移除流程

先回頭看看KeepAlive組件的渲染函數,它在渲染需要緩存的子組件時,會給它的VNode設置一個標記COMPONENT_SHOULD_KEEP_ALIVE,表示這個組件需要被緩存,這個標記會在兩個地方會被用到,一是組件的初次渲染,二是組件卸載,接下來我們來分別看看這兩個地方具體是幹了什麼。

4.1.1初次渲染

初次渲染的核心流程代碼在runtime-core/src/renderer.ts這個文件中,這個文件裏面包含了VNode patch的核心流程。組件初次渲染的函數調用順序是patch -> processComponent -> mountComponent -> setupRenderEffect -> componentUpdateFn,我們直接來看componentUpdateFn中的部分定義,因為COMPONENT_SHOULD_KEEP_ALIVE在這個方法中被用到了。

const setupRenderEffect: SetupRenderEffectFn = (
  instance,
  initialVNode,
  container,
  anchor,
  parentSuspense,
  namespace: ElementNamespace,
  optimized,
) => {
  const componentUpdateFn = () => {
    if (!instance.isMounted) {
      // 組件未掛載
      let vnodeHook: VNodeHook | null | undefined
      const { el, props } = initialVNode
      const { bm, m, parent, root, type } = instance
      const isAsyncWrapperVNode = isAsyncWrapper(initialVNode)

      if (bm) {
        // 調用beforeMount鈎子函數
        invokeArrayFns(bm)
      }

      //
      // 中間掛載過程中的代碼
      //

      if (m) {
        // 掛載完成後調用mounted鈎子函數
        queuePostRenderEffect(m, parentSuspense)
      }

      if (
        initialVNode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE ||
        (parent &&
          isAsyncWrapper(parent.vnode) &&
          parent.vnode.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE)
      ) {
        // 組件初次渲染後,如果是被keep-alive包裹的組件,則調用activated鈎子函數
        instance.a && queuePostRenderEffect(instance.a, parentSuspense)
      }
      // 標記組件已掛載
      instance.isMounted = true
    } else {
      // 組件已掛載
    }
  }
}

可以看到他的第一個作用就是在組件初次渲染後,如果是被keep-alive包裹的組件,則調用activated鈎子函數。

4.1.2組件卸載

vnode卸載時會統一調用renderer.ts中定義的unmount方法

const unmount: UnmountFn = (
  vnode,
  parentComponent,
  parentSuspense,
  doRemove = false,
  optimized = false,
) => {
  const { type, props, ref, children, dynamicChildren, shapeFlag, patchFlag, dirs, cacheIndex } =
    vnode

  // 卸載vnode時,如果組件被keepalive緩存,則調用keepalive內部定義的deactivate方法
  if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
    ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
    return
  }

  // 省去其他代碼
}

我們可以看到在卸載時,如果遇到了這個標記就不會繼續執行後續的卸載邏輯,而是調用了KeepAlive內部定義的deactivate方法。

const storageContainer = createElement('div')

sharedContext.deactivate = (vnode: VNode) => {
  const instance = vnode.component!

  // 把組件的節點從當前頁面上 移動到臨時的容器節點中
  move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
  queuePostRenderEffect(() => {
    if (instance.da) {
      // 調用組件中定義的onDeactivated的鈎子函數
      invokeArrayFns(instance.da)
    }
    const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
    if (vnodeHook) {
      invokeVNodeHook(vnodeHook, instance.parent, vnode)
    }
    // 標記組件已失活
    instance.isDeactivated = true
  }, parentSuspense)
}

通過不斷深入分析,發現被緩存的組件卸載時,只會將他的Dom元素移動到臨時創建的div中,並且調用組件中定義的onDeactivated的鈎子函數

4.2恢復流程

還是回到KeepAlive的渲染函數,vnode要複用的時候,他會給vnode標記為COMPONENT_KEPT_ALIVE,表示這個組件是被緩存的組件。這個標記也是會在後面的patch流程中被使用到,組件恢復時的函數調用順序是patch -> processComponent

  const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => {
    n2.slotScopeIds = slotScopeIds
    if (n1 == null) {
      // 無舊vNode
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        // 被緩存的組件,則調用keepalive內部定義的activate方法
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          namespace,
          optimized,
        )
      } else {
       // 未被緩存的組件 重新掛載
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          optimized,
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }
可以看到在重新渲染時,如果遇到了這個標記就不會重新走初次掛載邏輯,而是調用了KeepAlive內部定義的activate方法。
    sharedContext.activate = (
      vnode,
      container,
      anchor,
      namespace,
      optimized,
    ) => {
      const instance = vnode.component!
      // 將Dom節點先恢復到頁面中
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // 還需要patch更新流程,因為組件的props可能會發生變化
      patch(
        instance.vnode,
        vnode,
        container,
        anchor,
        instance,
        parentSuspense,
        namespace,
        vnode.slotScopeIds,
        optimized,
      )
      queuePostRenderEffect(() => {
        instance.isDeactivated = false
        if (instance.a) {
          // 調用組件中定義的onActivated的鈎子函數
          invokeArrayFns(instance.a)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
      }, parentSuspense)
    }

通過不斷深入分析,發現被緩存的組件恢復時,只會將他的Dom元素重新移動到頁面中,防止組件的props變化還需要走一遍patch更新流程(關於為什麼要patch,下一個問題中會提到),最後調用組件中定義的onActivated的鈎子函數。

通過代碼的層層分析,可算是弄懂了KeepAlive是如何處理組件的移除和恢復的了。

5.失活的組件,依賴的數據更新了,它會重新渲染嗎

這個問題要分兩種況討論,第一種是由父組件傳入到子組件的數據props,第二種是組件自身內部的定義的數據或依賴的全局狀態

5.1 父組件傳入的props變化

要理解父組件傳入的props變化,會不會觸發失活的組件更新,我們需要知道子組件是如何使用父組件傳入的props的,所以還是需要結合一下相關的代碼做分析。
首先父組件傳遞給子組件的props是先綁定在子組件的VNode上面的,類似下面這樣

// 模板中這樣寫 傳遞count給子組件
<CompChild :count="count"></CompChild>

// 實際在渲染函數大概會長這樣
// 第一個參數就是組件本升,第二個參數就是傳遞給組件的props
_createBlock(_component_CompChild, { count: $setup.count }, null, 8 /* PROPS */, ["count"])

// 生成的VNode 大概會長這樣
{
   // 標記這是一個VNode
   __v_isVNode: true,
   // 類型,組件編譯後的js對象
   type: _component_CompChild,
   // props
   props: { count },
   // 動態參數
   dynamicProps: ['count'],
   key: null,
   // 組件實例
   component: null,
   // 渲染的dom
   el: null,
   patchFlag: 8,
   children: null
   // 還有一些其他屬性
}

傳給組件的參數會被記錄在組件VNodeprops屬性上,等接下來組件渲染的時候會被初始化到組件的實例上面。 組件初始化的時候會調用setupComponent這個方法,該方法定義在runtime-core/src/component.ts

// component.ts
export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false,
  optimized = false,
): Promise<void> | undefined {
  isSSR && setInSSRSetupState(isSSR)

  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  // 初始化組件props
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children, optimized || isSSR)

  // 執行setup函數,並處理setup結果
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined

  isSSR && setInSSRSetupState(false)
  return setupResult
}

// componentProps.ts
export function initProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null,
  isStateful: number, // result of bitwise flag comparison
  isSSR = false,
): void {
  const props: Data = {}
  const attrs: Data = createInternalObject()
  instance.propsDefaults = Object.create(null)

  setFullProps(instance, rawProps, props, attrs)

  if (isStateful) {
    // 組件實例上的props是在VNode的props基礎上包了一層shallowReactive淺層響應式
    instance.props = isSSR ? props : shallowReactive(props)
  } else {
   // 函數式組件
  }
  instance.attrs = attrs
}

所以傳遞給子組件的props最終會被賦值給組件實例的props屬性,並且會被轉換成淺層響應式數據。最終組件的渲染函數和setup函數用的也是被轉換後的props

當傳遞給組件的props變化的時候,首先會觸發父組件的的render函數重新運行,然後會生成新的子組件的VNode,然後就會進入patch更新流程,這時候子組件的新舊Vnode對比,新舊對比會調用updateProps更新組件的props

  // renderer.ts
  const updateComponentPreRender = (
    instance: ComponentInternalInstance,
    nextVNode: VNode,
    optimized: boolean,
  ) => {
    nextVNode.component = instance
    const prevProps = instance.vnode.props
    instance.vnode = nextVNode
    instance.next = null
    // 更新組件的參數
    updateProps(instance, nextVNode.props, prevProps, optimized)
    updateSlots(instance, nextVNode.children, optimized)

    pauseTracking()
    // props更新後 先觸發pre-flush watchers
    flushPreFlushCbs(instance)
    resetTracking()
  }
  
  // componentProps.ts
  export function updateProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null,
  rawPrevProps: Data | null,
  optimized: boolean,
): void {
   // 更新props的函數,這個函數代碼較多,就不貼了
   // 這個函數裏面會把組件實例的props屬性裏面的值更新成新的值
}

看完props的更新邏輯,就能回答上面的問題了,組件內部使用的props必須要patch完成之後才會變成最新的數據,所以在組件失活的時候,即使父組件傳入的props發生了變化,但是由於子組件內部使用的props數據並沒有發生變化,所以這時候子組件是不會重新渲染的,只有等組件重新恢復的時候,手動的調用patch,完成props更新,如果props發生變化才會觸發子組件的重新渲染。

5.2 全局狀態變化

相比較於props的更新邏輯,全局狀態變化會很好理解,其實就是依賴收集和派發更新的邏輯 只要組件內部依賴的任何狀態更新了,就會觸發組件的重新渲染,無論組件是否失活。

所以第五個問題的結論也顯而易見了,只有組件依賴的數據是父組件傳入的props並且這個props傳入的只是原始類型或者説是非響應式的數據對象,這時候即使外部數據發生了變化,那麼子組件在失活的時候是不會觸發重新渲染的,而除了這種情況以外,依賴的任何其他的響應式數據發生變化都是會觸發組件重新渲染的。

遇到過的問題

失活組件重新渲染導致的BUG

在第四個問題中討論過失活組件的DOM節點會被移動到一個臨時創建的div中,這個時候雖然DOM沒有被銷燬,但是DOM的父級已經變了。 説一個場景,比如我們在項目中大量的使用了Vant中的ListSticky這些組件,這些組件有一個特點他會去尋找最近的父級滾動節點作為滾動監聽的對象,由於失活的組件DOM已經被移動到其他的地方,然而組件更新還是會正常觸發,這時候List這類組件尋找最近的父級滾動節點可能會找的不對,所以等組件再次恢復時,可能就會看到List的上拉加載回調不會被執行了這麼一個奇怪的BUG。

結語

能夠徹底弄明白上面的所有問題,不僅可以把KeepAlive弄明白了,而且順帶的組件的渲染流程也能掌握的八九不離十了。

如果對您有所幫助,歡迎您點個關注,我會定時更新技術文檔,大家一起討論學習,一起進步。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.