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

版本:v3.3.4

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

html 复制代码
<!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函数中,开始渲染子元素。

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

patch

patch 源码

js 复制代码
// 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中,typeh1,即元素类型为 ELEMENT,因此会进入processElement函数,执行 patch 过程。

流程图

总的流程图如下👇:

下面我们来分析下每个步骤做的事情,逐一击破💥:

processElement

processElement 源码

js 复制代码
// 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

processElement 源码

js 复制代码
// 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
  ) {
    // 判断当前 VNode 是否是静态节点,并且是否已经被挂载到 DOM 上。如果是,则说明当前 VNode 可以被复用,只需要对已有的 DOM 元素进行克隆即可。
    // 在复用节点时,需要注意只有静态节点才能被复用,因此需要判断当前 VNode 是否是静态节点。此外,只有在生产环境下才能进行节点克隆,因为克隆的节点无法进行热更新。
    el = vnode.el = hostCloneNode(vnode.el)
  } else {
    // 创建节点
    el = vnode.el = hostCreateElement(
      vnode.type as string,
      isSVG,
      props && props.is,
      props
    )

    // 元素节点可以包含文本子节点和元素子节点。在挂载元素节点时,需要先挂载子节点,因为某些属性可能依赖于子节点的内容,例如 <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
          )
        }
      }
      /**
       * 首先判断当前节点的 props 对象中是否包含 value 属性。如果包含,则需要对 value 属性进行特殊处理。
        在处理 value 属性时,需要注意以下两点:
            1、value 属性的设置顺序可能会影响表单元素的最终值。例如,在设置 min 和 max 属性后再设置 value 属性,可能会导致 value 属性的值被覆盖。因此,在设置 value 属性时,需要保证它是在其他相关属性之后设置的。
            2、value 属性的更新可能需要被强制执行。例如,在某些情况下,表单元素的值可能会被外部代码修改,此时需要强制更新 value 属性的值。因此,在设置 value 属性时,需要使用 hostPatchProp 函数,并将第三个参数设置为 null,以确保 value 属性的更新能够被强制执行。
       */
      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. 在复用节点时,需要注意只有静态节点才能被复用,因此需要判断当前 VNode 是否是静态节点。此外,只有在生产环境下才能进行节点克隆,因为克隆的节点无法进行热更新。
  2. 首先执行hostCreateElement创建该VNode的原生element元素。
  3. 然后需要注意:元素节点可以包含文本子节点和元素子节点。在挂载元素节点时,需要先挂载子节点,因为某些属性可能依赖于子节点的内容,例如 <select> 元素的 value 属性
  4. 接着创建当前VNode的子节点,如果当前VNode的子节点是文本节点,则调用hostSetElementText设置当前节点的文本内容;如果当前节点下还有多个子节点,则调用mountChildren,进入patch流程,向下递归挂载子节点。
  5. 如果当前VNode上有props,则调用hostPatchProp初始化当前元素的props属性。
  6. 当前元素的属性都已经初始化完并且其子节点都已经挂载完,则将当前元素追加到父元素container

hostCreateElement

createElement 源码

js 复制代码
// 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 元素,其实执行的是 nodeOpscreateElement,在 createElement 中,调用了 document 的方法来创建 HTML 元素。

hostSetElementText

setElementText

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

逻辑比较简单,执行 hostSetElementText 设置节点的文本内容,其实执行的是 nodeOpssetElementText,通过节点的 textContent 属性来设置节点的文本内容。

mountChildren

mountChildren源码

js 复制代码
// 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

hostPatchProp源码

js 复制代码
// 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 {
    // 判断当前属性的键是否为 true-value 或 false-value。如果是,则说明当前属性是用于设置复选框元素的选中值的。
    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 元素的原生方法,初始化其属性。

但是在处理复选框元素的选中值时,需要注意以下几点:

  • 复选框元素的选中值可能是非字符串类型,例如布尔值、数字等。为了确保选中值能够正确地被存储,需要将选中值存储在 DOM 属性中,而不是存储在 VNodeprops 对象中。在上面代码中,使用了 _trueValue_falseValue 属性来存储复选框元素的选中值。
  • 复选框元素的选中值可能会被序列化为字符串。为了避免这种情况,需要将选中值存储在 DOM 属性中,并在更新时从 DOM 属性中读取选中值。

hostInsert

hostInsert源码

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

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

总结

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

相关推荐
一斤代码2 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子2 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年2 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子3 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina3 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路4 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说4 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409194 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding4 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js
布兰妮甜4 小时前
Vue+ElementUI聊天室开发指南
前端·javascript·vue.js·elementui