Vue3源码解析之 render(一)

1,884 阅读7分钟

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

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

前言

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

案例

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

<!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 行:

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 文件中:

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 文件下:

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

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

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 函数中:

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 方法。

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

// 1 ShapeFlags.ELEMENT 
00000000 00000000 00000000 00000001 

// 9 shapeFlag 
00000000 00000000 00000000 00001001 

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

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

  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 方法:

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 方法:

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

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 文件中:

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 方法:

 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 并返回。

render-createElement.png

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

render-vnode.png

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

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

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

render-text.png

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

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 文件中:

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 文件中:

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 元素:

render-class.png

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

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

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

render-insert.png

元素挂载渲染完毕,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(一)
  9. Vue3源码解析之 render(二)
  10. Vue3源码解析之 render(三)
  11. Vue3源码解析之 render(四)
  12. Vue3源码解析之 render component(一)
  13. Vue3源码解析之 render component(二)
  14. Vue3源码解析之 render component(三)
  15. Vue3源码解析之 render component(四)
  16. Vue3源码解析之 render component(五)
  17. Vue3源码解析之 diff(一)
  18. Vue3源码解析之 diff(二)
  19. Vue3源码解析之 compiler(一)
  20. Vue3源码解析之 compiler(二)
  21. Vue3源码解析之 compiler(三)
  22. Vue3源码解析之 createApp