banner
linzhe

linzhe

Mounting and Updating Process of Vue 3 Components

You can write unit tests in the Vue 3 source code and use the breakpoint debugging feature of the vscode vitest plugin to understand the entire flow of Vue components. For example, in the following unit test, there are two components, one as the parent component App and one as the child component Comp, used to test the mounting and updating of components.

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>`
  )
})

Mounting Process#

During the breakpoint debugging of render(h(App, null), root), it is found that the component mountComponent is first mounted because this is the first render. When entering the setupComponent function, it is used to handle props and slots and some initialization work. For example, when the return value of the setup function is an object, it proxies the return value of setup (proxyRefs(setupResult)), but the current test case will not go through this step because the current return is a render function.

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
}

When initializing the child component, since props are passed from the parent component, { value: innerNumber.value }, note that this is a number, not a ref. Therefore, in initProps, the props passed from the parent component will be converted into a shallowReactive reactive data. Note that users should not modify props inside the child component, and the interception of modifying props is implemented in the aforementioned 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
    // Why use shallowReactive to wrap props? This will be explained later
    instance.props = isSSR ? props : shallowReactive(props)
  }
}

Next, the render function uses setupRenderEffect's componentUpdateFn (to be used later) for dependency collection and rendering.

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

Updating Process#

When modifying number.value = 3, since dependency collection will first re-execute the render of the App component, then patching occurs. When patching to the child component, since props have changed, the child component instance will update the effect function again.

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  const instance = (n2.component = n1.component)!
  // Why is Vue an update at the component level? This will be explained later
  if (shouldUpdateComponent(n1, n2, optimized)) {
    ...
    // Since props have changed, the child component instance will update the effect function again
    instance.effect.dirty = true
    instance.update()
  } else {
    // no update needed. just copy over properties
    n2.el = n1.el
    instance.vnode = n2
  }
  ...
}

When the child component update is re-executed, it will update Props and Slots, re-execute the child component render to get the latest vnode, and perform the patch update operation, completing the update of the child component.

// Child component update instance.update()
const componentUpdateFn = ()=>{
  ...
  updateComponentPreRender(instance, next, optimized)
  ...
  // After the update is complete, get the child component's vnode again, which will re-execute the child component's render
  const nextTree = renderComponentRoot(instance)
  // Execute patch update operation
  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
) => {
  ...
  // Update props
  updateProps(instance, nextVNode.props, prevProps, optimized)
  updateSlots(instance, nextVNode.children, optimized)
  ...
}

As for why to use shallowReactive to wrap props#

Because besides the render function, other effects will also use props, such as computed, etc. If props do not use a reactive object, then only the render function will re-execute, and other effect functions will not re-execute, which is a serious bug. Therefore, props must be a reactive object and can only be shallow because the child component only cares about changes in props.x, not changes in props.x.a. However, in some cases, there may be code like this, directly passing an object, which actually means that props.value has not been updated, equivalent to innerNumber again relying on the collection of the child component's render function, and the official documentation does not recommend this writing.

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>`
  )
})

As for why Vue is an update at the component level#

For example, in the following case pure component, where Comp is a component without props, when the parent component changes, shouldUpdateComponent will return false, and the update branch for this child component will not be executed. Therefore, only the render functions between the parent and child will be re-executed, and the render function of the child component will not be re-executed. Thus, Vue is an update at the component level.

test('pure component', async () => {
  const number = ref(1)
  const App = {
    setup() {
      const innerNumber = number
      return () => {
        // After number.value = 3, it will print 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 () => {
        // After number.value = 3, it will not print 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
  ...
  // Here explains why Vue is an update at the component level
  // For example, in the aforementioned test('pure component') unit test
  // Both prevProps and nextProps are null, so return false,
  // and the update branch for this child component will not be executed, and instance.update() will not be executed.
  if (prevProps === nextProps) {
    return false
  }
  ...
  return false
}
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.