Vue3源码解析之 render(一)

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 8 篇,关注专栏

前言

runtime 文中我们了解到,Vue 通过 h 函数生成 VNode 对象,再通过 render 函数将 VNode 对象渲染为真实 DOM。由于 render 函数涉及到 DOM 的渲染、更新、删除等,本篇我们先来看下 render 函数是如何实现 DOM 渲染的。

案例

首先引入 hrender 两个函数,之后通过 h 函数生成一个 vnode 对象,最后将 vnode 对象通过 render 函数渲染为真实 DOM

html 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { h, render } = Vue

      const vnode = h(
        'div',
        {
          class: 'test'
        },
        'hello render'
      )

      render(vnode, document.querySelector('#app'))
    </script>
  </body>
</html>

render 实现

render 函数定义在 packages/runtime-core/src/renderer.ts 文件下,大致在 2341 行:

ts 复制代码
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // 省略
  
  const render: RootRenderFunction = (vnode, container, isSVG) => {
    if (vnode == null) {
      if (container._vnode) {
        unmount(container._vnode, null, null, true)
      }
    } else {
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    container._vnode = vnode
  }
  
  // 省略
  
  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

render 函数实际是 baseCreateRenderer 函数暴露出来的一个对象方法,但在我们使用时是直接调用 render 函数,那 Vue 是如何导出该方法的呢?

我们知道 Vue 中 runtime-core 文件夹是运行时的核心部分,主要对虚拟 DOM 的处理等,是与平台(例如浏览器、服务器端渲染)无关的部分,它可以在不同的平台上运行,只需要配合相应的渲染器(比如 runtime-dom)来实现具体的渲染逻辑。

runtime-dom 文件夹是针对浏览器环境的具体渲染器,包含了与浏览器环境相关的操作,比如处理 DOM 元素、事件处理、属性更新等。

所以 render 函数导出实际被定义在 packages/runtime-dom/src/index.ts 文件中:

ts 复制代码
export const render = ((...args) => {
  ensureRenderer().render(...args)
}) as RootRenderFunction<Element | ShadowRoot>

function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

可以看出 render 函数实际执行的是 ensureRenderer().render(...args),而 ensureRenderer 函数实际执行的是 createRenderer 方法,该方法定义在 packages/runtime-core/src/renderer.ts 文件下:

ts 复制代码
export function createRenderer<
  HostNode = RendererNode,
  HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
  return baseCreateRenderer<HostNode, HostElement>(options)
}

可想而知,render 函数真正执行的是 baseCreateRenderer 函数返回对象中的 render 方法。那么理解完 render 函数的调用和导出,我们再回过来看下 render 函数的实现逻辑:

ts 复制代码
const render: RootRenderFunction = (vnode, container, isSVG) => {
    // vnode 不存在
    if (vnode == null) {
      // 存在旧节点
      if (container._vnode) {
        // 卸载旧节点
        unmount(container._vnode, null, null, true)
      }
    } else {
      // 更新节点
      patch(container._vnode || null, vnode, container, null, null, null, isSVG)
    }
    flushPostFlushCbs()
    // _vnode 赋值旧节点
    container._vnode = vnode
  }

根据案例,当前 vnode 存在,直接走 patch 方法,该方法也是被定义在 baseCreateRenderer 函数中:

ts 复制代码
const patch: PatchFn = (
    n1, // 旧节点
    n2, // 新节点
    container, // 容器
    anchor = null, // 锚点
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    // 新旧节点是否相同
    if (n1 === n2) {
      return
    }

    // patching & not same type, unmount old tree
    // 存在旧节点 且 新旧节点类型是否相同
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }

    if (n2.patchFlag === PatchFlags.BAIL) {
      optimized = false
      n2.dynamicChildren = null
    }

    const { type, ref, shapeFlag } = n2
    // 根据 新节点类型 判断
    switch (type) {
      case Text:
        processText(n1, n2, container, anchor)
        break
      case Comment:
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:
        if (n1 == null) {
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }

我们只需关注前四个参数:n1 旧节点n2 新节点container 容器anchor 锚点。由于第一次渲染,所以此时 n1 为 null,这里的 锚点 也比较关键, 具体逻辑我们稍后分析。之后根据 n2 新节点 类型走不同的逻辑,当前新节点 type 类型为 div,所以走 default 逻辑。

接着执行判断逻辑 shapeFlag & ShapeFlags.ELEMENT,当前 shapeFlag 为 9, ShapeFlags.ELEMENT 为 1,按位与 运算结果为 1,if(1) 为真,执行 processElement 方法。

这里拓展下 & 按位与,和之前 按位或 相似,都是转为二进制后计算:

ts 复制代码
// 1 ShapeFlags.ELEMENT 
00000000 00000000 00000000 00000001 

// 9 shapeFlag 
00000000 00000000 00000000 00001001 

// 与 运算 上下为 1 则为 1 否则为 0 
// 结果 1 
00000000 00000000 00000000 00000001

我们再看下 processElement 方法,该方法也是被定义在 baseCreateRenderer 函数中:

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(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    } else {
     // 更新
      patchElement(
        n1,
        n2,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      )
    }
  }

由于当前旧节点不存在,直接走 mountElement 方法:

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 {
      // 执行 createElement 方法
      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) {
        // setElementText 方法
        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)) {
            // 执行 runtime-dom/src/patchProp.ts中的 patchProp 方法
            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)
    }
    // 省略
    
    // 插入到 container 中
    hostInsert(el, container, anchor)
    
    // 省略
  }

由于此时 vnode.el 不存在,直接走 el = vnode.el = hostCreateElement() 对其赋值,我们再看下 hostCreateElement 方法:

ts 复制代码
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
): any {
  // 省略

  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent
  } = options
  
  // 省略
}  

该方法是通过传入的 options 参数解构得到的,我们知道 render 函数执行的是 ensureRenderer().render(...args),而 ensureRenderer 执行的是 createRenderer,等同于执行 baseCreateRenderer 方法,而参数是在执行 createRenderer 时传入的 rendererOptions

ts 复制代码
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)

export const render = ((...args) => {
  ensureRenderer().render(...args)
}) as RootRenderFunction<Element | ShadowRoot>

function ensureRenderer() {
  return (
    renderer ||
    (renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
  )
}

可以看出 rendererOptions 参数实际是 { patchProp }, nodeOps 合并后的对象,我们主要看下 nodeOps 对象,它被定义在 packages/runtime-dom/src/nodeOps.ts 文件中:

ts 复制代码
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
  insert: (child, parent, anchor) => {
    // 将 child 插入 锚点之前
    // 执行完 页面会渲染完成
    parent.insertBefore(child, anchor || null)
  },

  remove: child => {
    const parent = child.parentNode
    if (parent) {
      parent.removeChild(child)
    }
  },

  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
  },

  createText: text => doc.createTextNode(text),

  createComment: text => doc.createComment(text),

  setText: (node, text) => {
    node.nodeValue = text
  },

  setElementText: (el, text) => {
    el.textContent = text
  },

  parentNode: node => node.parentNode as Element | null,

  nextSibling: node => node.nextSibling,

  querySelector: selector => doc.querySelector(selector),

  setScopeId(el, id) {
    el.setAttribute(id, '')
  },

  cloneNode(el) {
    const cloned = el.cloneNode(true)
    // #3072
    // - in `patchDOMProp`, we store the actual value in the `el._value` property.
    // - normally, elements using `:value` bindings will not be hoisted, but if
    //   the bound value is a constant, e.g. `:value="true"` - they do get
    //   hoisted.
    // - in production, hoisted nodes are cloned when subsequent inserts, but
    //   cloneNode() does not copy the custom property we attached.
    // - This may need to account for other custom DOM properties we attach to
    //   elements in addition to `_value` in the future.
    if (`_value` in el) {
      ;(cloned as any)._value = (el as any)._value
    }
    return cloned
  },

  // __UNSAFE__
  // Reason: innerHTML.
  // Static content here can only come from compiled templates.
  // As long as the user only uses trusted templates, this is safe.
  insertStaticContent(content, parent, anchor, isSVG, start, end) {
    // <parent> before | first ... last | anchor </parent>
    const before = anchor ? anchor.previousSibling : parent.lastChild
    // #5308 can only take cached path if:
    // - has a single root node
    // - nextSibling info is still available
    if (start && (start === end || start.nextSibling)) {
      // cached
      while (true) {
        parent.insertBefore(start!.cloneNode(true), anchor)
        if (start === end || !(start = start!.nextSibling)) break
      }
    } else {
      // fresh insert
      templateContainer.innerHTML = isSVG ? `<svg>${content}</svg>` : content
      const template = templateContainer.content
      if (isSVG) {
        // remove outer svg wrapper
        const wrapper = template.firstChild!
        while (wrapper.firstChild) {
          template.appendChild(wrapper.firstChild)
        }
        template.removeChild(wrapper)
      }
      parent.insertBefore(template, anchor)
    }
    return [
      // first
      before ? before.nextSibling! : parent.firstChild!,
      // last
      anchor ? anchor.previousSibling! : parent.lastChild!
    ]
  }
}

nodeOps 对象主要定义了一些浏览器相关的方法,比如 DOM 处理、事件处理、属性更新等。我们回过来再看下 hostCreateElement 方法,实际执行的是 createElement 方法:

ts 复制代码
 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
  },

当前 tag 为传入的 vnode.typediv,通过 document.createElement 创建了一个 div 元素赋值给 el 并返回。

此时 vnode.el 就挂载 div 元素:

接着执行 shapeFlag & ShapeFlags.TEXT_CHILDREN 判断,shapeFlag 为 9,ShapeFlags.TEXT_CHILDREN 为 8,按位与运算得出结果是 8,if(8) 结果为真,执行 hostSetElementText 方法,可以看出所有前缀 host 方法都是浏览器相关操作,都被定义在 nodeOps 对象中,该方法实际执行的是 setElementText 方法:

ts 复制代码
setElementText: (el, text) => {
    el.textContent = text
  },

el 参数为之前创建的 div 元素,而 text 参数为传入的 vnode.childrenhello render,所以此时 div 元素的 innderHTMLinnderText 被赋值为 hello render

子节点挂载完毕,之后挂载 prop 属性,当前 props{ class: 'test' }

ts 复制代码
if (props) {
    for (const key in props) {
      if (key !== 'value' && !isReservedProp(key)) {
        // 执行 runtime-dom/src/patchProp.ts中的 patchProp 方法
        hostPatchProp(
          el,
          key,
          null,
          props[key],
          isSVG,
          vnode.children as VNode[],
          parentComponent,
          parentSuspense,
          unmountChildren
        )
      }
    }
    
    // 省略
}

hostPatchProp 方法实际执行的是 patchProp,它被定义在 packages/runtime-dom/src/patchProp.ts 文件中:

ts 复制代码
export const patchProp: DOMRendererOptions['patchProp'] = (
  el,
  key,
  prevValue,
  nextValue,
  isSVG = false,
  prevChildren,
  parentComponent,
  parentSuspense,
  unmountChildren
) => {
  if (key === 'class') {
    // runtime-dom/src/modules/class.ts 中
    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)
  }
}

我们主要关注前四个参数,el 为当前 div 元素,keyclassprevValuenullnextValueprops[key]test,由于当前 keyclass,执行 patchClass 方法,该方法定义在 packages/runtime-dom/src/modules/class.ts 文件中:

ts 复制代码
export function patchClass(el: Element, value: string | null, isSVG: boolean) {
  // directly setting className should be faster than setAttribute in theory
  // if this is an element during a transition, take the temporary transition
  // classes into account.
  const transitionClasses = (el as ElementWithTransition)._vtc
  if (transitionClasses) {
    value = (
      value ? [value, ...transitionClasses] : [...transitionClasses]
    ).join(' ')
  }
  if (value == null) {
    el.removeAttribute('class')
  } else if (isSVG) {
    el.setAttribute('class', value)
  } else {
    el.className = value
  }
}

该方法就是通过 DOM 的方法、属性来设置或移除 class,所以此时 vnode.el 赋值为带有 classtestdiv 元素:

之后执行 hostInsert(el, container, anchor) 方法,该方法实际执行的是 insert 方法:

ts 复制代码
 insert: (child, parent, anchor) => {
    // 将 child 插入 锚点之前
    // 执行完 页面会渲染完成
    parent.insertBefore(child, anchor || null)
  },

而之前提到的 anchor 锚点,就是为了将子节点插入到锚点之前 。当前 childdiv.test 元素,parentdiv#app 元素,执行完 parent.insertBefore(child, anchor || null) 页面渲染完成。

元素挂载渲染完毕,render 函数执行 container._vnode = vnode,将新节点 vnode 赋值到旧节点_vnode 上,render 函数执行完毕。

总结

  1. render 函数触发 patch 方法,在 patch 方法中根据新节点的 typeshapeFlag 值决定当前哪一种类型的节点挂载。再根据 新旧节点 决定是 挂载 还是 更新 ,取决于旧节点是否存在。
  2. 挂载过程分为四大步:
    a. 创建 div 标签,即 hostCreateElement()
    b. 生成标签里的 text,即 hostSetElementText()
    c. 处理 prop 属性,即 hostPatchProp()
    d. 插入 DOM,即 hostInsert()
  3. 最后挂载旧节点 _vnode

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
  5. Vue3源码解析之 watch
  6. Vue3源码解析之 runtime
  7. Vue3源码解析之 h
  8. Vue3源码解析之 render(一)
相关推荐
蟾宫曲3 小时前
在 Vue3 项目中实现计时器组件的使用(Vite+Vue3+Node+npm+Element-plus,附测试代码)
前端·npm·vue3·vite·element-plus·计时器
秋雨凉人心3 小时前
简单发布一个npm包
前端·javascript·webpack·npm·node.js
liuxin334455663 小时前
学籍管理系统:实现教育管理现代化
java·开发语言·前端·数据库·安全
qq13267029403 小时前
运行Zr.Admin项目(前端)
前端·vue2·zradmin前端·zradmin vue·运行zradmin·vue2版本zradmin
LCG元4 小时前
Vue.js组件开发-使用vue-pdf显示PDF
vue.js
魏时烟4 小时前
css文字折行以及双端对齐实现方式
前端·css
哥谭居民00015 小时前
将一个组件的propName属性与父组件中的variable变量进行双向绑定的vue3(组件传值)
javascript·vue.js·typescript·npm·node.js·css3
烟波人长安吖~5 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
2401_882726485 小时前
低代码配置式组态软件-BY组态
前端·物联网·低代码·前端框架·编辑器·web
web130933203985 小时前
ctfshow-web入门-文件包含(web82-web86)条件竞争实现session会话文件包含
前端·github