banner
linzhe

linzhe

vue3 組件的掛載更新流程

可以在 vue3 源碼中編寫單元測試,並通過vscode的vitest插件斷點調試功能,
就可以知道 vue 組件的整個流程,比如下面這個單測,其中包含了兩個組件,其中一個作為父組件App
一個作為子組件Comp,用於測試組件的掛載和更新

test('basic component', async () => {
  const number = ref(1)
  const App = {
    setup() {
      const innerNumber = number
      return () => {
        console.log('app render')
        return h('div', { id: 'test-id', class: 'test-class' }, [
          h(Comp, { value: innerNumber.value }),
        ])
      }
    },
  }
  const Comp = {
    props: ['value'],
    setup(props: any) {
      const x = computed(() => props.value)
      return () => {
        console.log('son render')
        return h('span', null, 'number ' + x.value)
      }
    },
  }

  const root = nodeOps.createElement('div')
  render(h(App, null), root)
  let innerStr = serializeInner(root)
  expect(innerStr).toBe(
    `<div id="test-id" class="test-class"><span>number 1</span></div>`
  )
  number.value = 3
  await nextTick()
  innerStr = serializeInner(root)
  expect(innerStr).toBe(
    `<div id="test-id" class="test-class"><span>number 3</span></div>`
  )
})

掛載流程#

在斷點調試render(h(App, null), root)的過程中,發現首先會進行掛載組件mountComponent,因為這是第一次渲染,在進入setupComponent函數,
用於處理 props 和 slots 和一些初始化工作,比如當 setup 函數的返回值是一個對象的時候,代理 setup 的返回值 (proxyRefs(setupResult)),但是當前的
測試用例並不會走這一步,因為當前返回的是一個渲染函數

const mountComponent = () => {
  ...
  setupComponent(...);
  ...
  setupRenderEffect(...);
  ...
};

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  // ...
  const { props, children } = instance.vnode
  const isStateful = isStatefulComponent(instance)
  initProps(instance, props, isStateful, isSSR)
  initSlots(instance, children)

  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined

  isSSR && setInSSRSetupState(false)
  return setupResult
}

當初始化子組件時,因為在父組件傳入了 props,{ value: innerNumber.value },注意這是一個數字,而不是一個 ref,所以在 initProps 中,會把父組件傳遞的 props 轉換成一個shallowReactive響應式的數據,
注意用戶在子組件裡面不應該修改 props,並且修改 props 攔截操作就在上文提到的setupStatefulComponent中實現 (instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)))

export function initProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null,
  isStateful: number, // result of bitwise flag comparison
  isSSR = false
) {
  const props: Data = {}
  if (isStateful) {
    // stateful
    // 為什麼要是用shallowReactive包裹props?下文會進行解釋
    instance.props = isSSR ? props : shallowReactive(props)
  }
}

接下來對渲染函數使用setupRenderEffectcomponentUpdateFn(下文會用到)進行依賴收集,並且進行渲染

expect(innerStr).toBe(
  `<div id="test-id" class="test-class"><span>number 1</span></div>`
)

更新流程#

當修改了number.value = 3,由於依賴收集首先會重新執行 App 組件的 render,然後在進行 patch,當 patch 到子組件時,
由於 props 發生了變化,則子組件實例會重新更新副作用函數

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  const instance = (n2.component = n1.component)!
  // 為什麼vue是組件級別的更新?下文會進行解釋
  if (shouldUpdateComponent(n1, n2, optimized)) {
    ...
    // 由於props發生了變化,則子組件實例會重新更新副作用函數
    instance.effect.dirty = true
    instance.update()
  } else {
    // no update needed. just copy over properties
    n2.el = n1.el
    instance.vnode = n2
  }
  ...
}

當重新執行子組件更新時,就會更新 Props 和 Slots,並重新執行子組件 render 獲取最新的 vnode,並執行 patch 更新操作,然後子組件就更新完成了

// 子組件的更新 instance.update()
const componentUpdateFn = ()=>{
  ...
  updateComponentPreRender(instance, next, optimized)
  ...
  // 更新完成重新得到子組件的vnode,即會重新執行子組件的render
  const nextTree = renderComponentRoot(instance)
  // 執行patch更新操作
  patch(
    prevTree,
    nextTree,
    // parent may have changed if it's in a teleport
    hostParentNode(prevTree.el!)!,
    // anchor may have changed if it's in a fragment
    getNextHostNode(prevTree),
    instance,
    parentSuspense,
    namespace,
  )
}

const updateComponentPreRender = (
  instance: ComponentInternalInstance,
  nextVNode: VNode,
  optimized: boolean
) => {
  ...
  // 更新props
  updateProps(instance, nextVNode.props, prevProps, optimized)
  updateSlots(instance, nextVNode.children, optimized)
  ...
}

至於為什麼要用 shallowReactive 包裹 props#

因為除了渲染函數,其他副作用也會使用 props,如 computed 等,
如果 props 不使用響應式對象,那麼只有渲染函數會重新執行,其他的副作用函數,就不會重新執行了,這是一個很嚴重的 bug,
所以 props 必須是響應式對象,並且也只能是淺的,因為子組件只關心props.x變化了,不關心props.x.a變化了
但是有些情況下,會有如下這種代碼,直接傳遞一個對象,這種其實 props.value 並沒有更新,相當於 innerNumber
又依賴收集了子組件的渲染函數,並且官方文檔不推薦這種寫法

test('basic component', async () => {
  const App = {
    setup() {
      const innerNumber = reactive({ data: 1 })
      return () => {
        console.log('app render')
        return h('div', { id: 'test-id', class: 'test-class' }, [
          h(Comp, { value: innerNumber }),
        ])
      }
    },
  }
  const Comp = {
    props: ['value'],
    setup(props: any) {
      onMounted(async () => {
        props.value.data = 3
        await nextTick()
        innerStr = serializeInner(root)
        expect(innerStr).toBe(
          `<div id="test-id" class="test-class"><span>number 3</span></div>`
        )
      })
      return () => {
        console.log('son render')
        return h('span', null, 'number ' + props.value.data)
      }
    },
  }

  const root = nodeOps.createElement('div')
  render(h(App, null), root)
  let innerStr = serializeInner(root)
  expect(innerStr).toBe(
    `<div id="test-id" class="test-class"><span>number 1</span></div>`
  )
})

至於為什麼 vue 是組件級別的更新#

比如下面這個案例pure component,其中的這個Comp是一個沒有props的組件,在父組件變更的時候shouldUpdateComponent會返回false
就不會走更新這個子組件的分支了,那麼就只有父子間的 render 函數會重新執行,子組件的 render 函數就不會重新執行,所以vue是組件級別的更新

test('pure component', async () => {
  const number = ref(1)
  const App = {
    setup() {
      const innerNumber = number
      return () => {
        // number.value = 3 後,會打印 app render
        console.log('app render')
        return h(
          'div',
          { id: 'test-id-' + innerNumber.value, class: 'test-class' },
          [h(Comp)],
        )
      }
    },
  }
  const Comp = {
    setup(props: any) {
      return () => {
        // number.value = 3 後,不會打印 son render
        console.log('son render')
        return h('span', null, 'number')
      }
    },
  }
  const root = nodeOps.createElement('div')
  render(h(App, null), root)
  number.value = 3
  await nextTick()
})

export function shouldUpdateComponent(
  prevVNode: VNode,
  nextVNode: VNode,
  optimized?: boolean,
): boolean {
  const { props: prevProps } = prevVNode
  const { props: nextProps } = nextVNode
  ...
  // 這裡就是解釋了為什麼vue是組件級別的更新了
  // 就比如上文提到的test('pure component')這個單測
  // 那麼prevProps和nextProps都是null,所以return false,
  // 就不會走更新這個子組件的分支了 不會執行 instance.update()
  if (prevProps === nextProps) {
    return false
  }
  ...
  return false
}
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。