Vue3源码解读之首次渲染DOM树

97 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第20天,点击查看活动详情 >>

版本:3.2.31

在首次渲染过程中,完成了根组件实例的挂载后,Vue3会将template的内容编译后存放在根组件实例的 render属性上(具体实现可参阅vue3源码解读之初始化流程中的finishComponentSetup)。然后在开始渲染根组件时执行当前根组件实例的render函数获取子元素的VNode,将子元素的VNode传入patch函数中,递归渲染子元素(具体实现可参阅vue3源码解读之初始化流程中的setupRenderEffect)。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h1>{{title}}</h1>
    </div>
    <script src="../dist/vue.global.js"></script>
    <script>
      // 1.创建实例
      // vue3: createApp()
      const { createApp } = Vue
      // 传入根组件配置
      const app = createApp({
        data() {
          return {
            title: 'hello,vue3!'
          }
        },
      }).mount('#app')
    </script>
  </body>
</html>

在上面的HTML代码中,根组件实例的template如下:

执行根组件实例的render函数后获取的子元素VNode则如下图:

将该VNode传入patch函数中,开始渲染子元素。

// 进入 Diff 过程,将子树渲染到container中
patch(
  null,
  subTree,
  container,
  anchor,
  instance,
  parentSuspense,
  isSVG
)

patch

// core/packages/runtime-core/src/renderer.ts

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {

    // ...

    const { type, ref, shapeFlag } = n2
    switch (type) {
      // ...
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } 
        // ...
    }

   // ...
  }

在上面的subTree中,type为h1,即元素类型为 ELEMENT,因此会进入processElement函数,执行patch过程。

processElement

// core/packages/runtime-core/src/renderer.ts

const processElement = (
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  isSVG = isSVG || (n2.type as string) === 'svg'
  if (n1 == null) {
    // 首次渲染执行 mountElement 挂载 ELEMENT
    mountElement(
      n2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  } else {
    // 更新过程
  }
}

由于是首次渲染,因此执行 mountElement函数渲染 ELEMENT 类型的元素。

mountElement

// core/packages/runtime-core/src/renderer.ts

const mountElement = (
  vnode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  let el: RendererElement
  let vnodeHook: VNodeHook | undefined | null
  const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
  if (
    !__DEV__ &&
    vnode.el &&
    hostCloneNode !== undefined &&
    patchFlag === PatchFlags.HOISTED
  ) {
    // If a vnode has non-null el, it means it's being reused.
    // Only static vnodes can be reused, so its mounted DOM nodes should be
    // exactly the same, and we can simply do a clone here.
    // only do this in production since cloned trees cannot be HMR updated.

    // 复用节点
    el = vnode.el = hostCloneNode(vnode.el)
  } else {
    // 创建节点
    el = vnode.el = hostCreateElement(
      vnode.type as string,
      isSVG,
      props && props.is,
      props
    )

    // mount children first, since some props may rely on child content
    // being already rendered, e.g. `<select value>`
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      // 设置节点的文本内容
      hostSetElementText(el, vnode.children as string)
    } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 当前节点下还有子节点,则向下递归挂载子节点
      mountChildren(
        vnode.children as VNodeArrayChildren,
        el,
        null,
        parentComponent,
        parentSuspense,
        isSVG && type !== 'foreignObject',
        slotScopeIds,
        optimized
      )
    }

    if (dirs) {
      invokeDirectiveHook(vnode, null, parentComponent, 'created')
    }
    // props
    // 如果元素有属性,则初始化这些属性
    if (props) {
      for (const key in props) {
        if (key !== 'value' && !isReservedProp(key)) {
          hostPatchProp(
            el,
            key,
            null,
            props[key],
            isSVG,
            vnode.children as VNode[],
            parentComponent,
            parentSuspense,
            unmountChildren
          )
        }
      }
      /**
       * Special case for setting value on DOM elements:
       * - it can be order-sensitive (e.g. should be set *after* min/max, #2325, #4024)
       * - it needs to be forced (#1471)
       * #2353 proposes adding another renderer option to configure this, but
       * the properties affects are so finite it is worth special casing it
       * here to reduce the complexity. (Special casing it also should not
       * affect non-DOM renderers)
       */
      if ('value' in props) {
        hostPatchProp(el, 'value', null, props.value)
      }
      if ((vnodeHook = props.onVnodeBeforeMount)) {
        invokeVNodeHook(vnodeHook, parentComponent, vnode)
      }
    }
    // scopeId
    setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
  }
  // ...

  // 将当前节点追加到父元素里
  hostInsert(el, container, anchor)

  // ...
}

在 mountElement 中:

  1. 首先执行hostCreateElement创建该VNode的原生element元素。

  2. 接着创建当前VNode的子节点,如果当前VNode的子节点是文本节点,则调用hostSetElementText设置当前节点的文本内容;如果当前节点下还有多个子节点,则调用mountChildren,进入patch流程,向下递归挂载子节点。

  3. 如果当前VNode上有props,则调用hostPatchProp初始化当前元素的props属性。

  4. 当前元素的属性都已经初始化完并且其子节点都已经挂载完,则将当前元素追加到父元素container中

hostCreateElement

// core/packages/runtime-dom/src/nodeOps.ts

// hostCreateElement 其实执行的是 createElement
createElement: (tag, isSVG, is, props): Element => {
    const el = isSVG
      ? doc.createElementNS(svgNS, tag)
      : doc.createElement(tag, is ? { is } : undefined)

    if (tag === 'select' && props && props.multiple != null) {
      ;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
    }

    return el
  },

执行hostCreateElement创建HTML元素,其实执行的是 nodeOps 的 createElement,在createElement中,调用了document的方法来创建HTML元素。

hostSetElementText

// core/packages/runtime-dom/src/nodeOps.ts
// hostSetElementText 其实执行的是 setElementText
setElementText: (el, text) => {
  el.textContent = text
},

执行 hostSetElementText设置节点的文本内容,其实执行的是 nodeOps 的 setElementText,通过节点的 textContent 属性来设置节点的文本内容。

mountChildren

// core/packages/runtime-core/src/renderer.ts

const mountChildren: MountChildrenFn = (
  children,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  slotScopeIds,
  optimized,
  start = 0
) => {
  for (let i = start; i < children.length; i++) {
    const child = (children[i] = optimized
      ? cloneIfMounted(children[i] as VNode)
      : normalizeVNode(children[i]))
    patch(
      null,
      child,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

在mountChildren中,遍历孩子节点,进入patch流程,向下递归挂载子节点。

hostPatchProp

// core/packages/runtime-dom/src/patchProp.ts

// hostPatchProp 实际上执行的 patchProp
export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) => {
  if (key === 'class') {
    patchClass(el, nextValue, isSVG)
  } else if (key === 'style') {
    patchStyle(el, prevValue, nextValue)
  } else if (isOn(key)) {
    // ignore v-model listeners
    if (!isModelListener(key)) {
      patchEvent(el, key, prevValue, nextValue, parentComponent)
    }
  } else if (
    key[0] === '.'
      ? ((key = key.slice(1)), true)
      : key[0] === '^'
      ? ((key = key.slice(1)), false)
      : shouldSetAsProp(el, key, nextValue, isSVG)
  ) {
    patchDOMProp(
      el,
      key,
      nextValue,
      prevChildren,
      parentComponent,
      parentSuspense,
      unmountChildren
    )
  } else {
    // special case for <input v-model type="checkbox"> with
    // :true-value & :false-value
    // store value as dom properties since non-string values will be
    // stringified.
    if (key === 'true-value') {
      ;(el as any)._trueValue = nextValue
    } else if (key === 'false-value') {
      ;(el as any)._falseValue = nextValue
    }
    patchAttr(el, key, nextValue, isSVG, parentComponent)
  }
}

在初始化元素的 props 时,根据属性名,调用DOM元素的原生方法,初始化其属性。

hostInsert

// core/packages/runtime-dom/src/nodeOps.ts
// hostInsert 实际上执行的是 insert
insert: (child, parent, anchor) => {
  parent.insertBefore(child, anchor || null)
},

执行hostInsert,实际执行的是nodeOps的insert,通过节点的insertBefore方法,将子节点的内容插入到父节点中。

流程图

总结

在首次渲染过程中,完成根组件实例的挂载后,获取template的虚拟DOM,将其传入patch函数中,递归渲染子元素。在子元素的渲染过程中,会首先创建节点,然后创建当前节点的子元素。如果当前节点上有 props,则初始化当前节点的props属性。最后将当前元素追加到父元素container中。