Vue3源码解析之 h

931 阅读9分钟

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

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

前言

上篇 runtime 文中我们了解到,虚拟 DOM 是 Vue 在运行时,通过 h 函数获取到 VNode 对象,本篇我们就来看下 h 函数是如何实现的。

案例

首先引入 h 函数,之后通过 h 函数生成一个 vnode 对象,并将其打印。

<!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 } = Vue

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

      console.log(vnode)
    </script>
  </body>
</html>

h 实现

h 函数定义在 packages/runtime-core/src/h.ts 文件下:

export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length // 参数长度
  // 参数为 2个
  if (l === 2) {
    // propsOrChildren 是否为对象 且不为数组
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // single vnode without props
      // propsOrChildren 是否为 vnode
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }
      // props without children
      return createVNode(type, propsOrChildren)
    } else {
      // omit props
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    // 参数超过 3个
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      // 参数为3个, children是否是 vnode
      children = [children]
    }
    // 当前案例 直接走该逻辑
    return createVNode(type, propsOrChildren, children)
  }
}

可以看出 h 函数接收三个参数,当前 typedivpropsOrChildren{ class: 'test'}childrenhello render。之后根据参数的长度不同走不同的判断逻辑,其核心是执行 createVNode 方法,实际执行的是 _createVNode,该方法在 packages/runtime-core/src/vnode.ts 文件中:

export const createVNode = (
  __DEV__ ? createVNodeWithArgsTransform : _createVNode
) as typeof _createVNode

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode {
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    if (__DEV__ && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}.`)
    }
    type = Comment
  }
  // 是否是 vnode 通过 __v_isVNode 来判断
  if (isVNode(type)) {
    // createVNode receiving an existing vnode. This happens in cases like
    // <component :is="vnode"/>
    // #2078 make sure to merge refs during the clone instead of overwriting it
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)
    }
    if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
      if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
        currentBlock[currentBlock.indexOf(type)] = cloned
      } else {
        currentBlock.push(cloned)
      }
    }
    cloned.patchFlag |= PatchFlags.BAIL
    return cloned
  }

  // class component normalization.
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // 2.x async/functional component compat
  if (__COMPAT__) {
    type = convertLegacyComponent(type, currentRenderingInstance)
  }

  // class & style normalization.
  // class 和 style 的增强
  if (props) {
    // for reactive or proxy objects, we need to clone it to enable mutation.
    props = guardReactiveProps(props)! // 解析 props
    let { class: klass, style } = props // 结构  class 赋值给 klass, style
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass) // 增强 class
    }
    if (isObject(style)) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type) // 根据 type 类型进行 shapeFlag 赋值 当前为 div 则 ShapeFlags.ELEMENT
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
    ? ShapeFlags.SUSPENSE
    : isTeleport(type)
    ? ShapeFlags.TELEPORT
    : isObject(type)
    ? ShapeFlags.STATEFUL_COMPONENT
    : isFunction(type)
    ? ShapeFlags.FUNCTIONAL_COMPONENT
    : 0

  if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
    type = toRaw(type)
    warn(
      `Vue received a Component which was made a reactive object. This can ` +
        `lead to unnecessary performance overhead, and should be avoided by ` +
        `marking the component with \`markRaw\` or using \`shallowRef\` ` +
        `instead of \`ref\`.`,
      `\nComponent that was made reactive: `,
      type
    )
  }

  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )
}

这里 isVNode(type) 通过判断 type 是否是 VNode,我们来看下 isVNode 方法:

export function isVNode(value: any): value is VNode {
  return value ? value.__v_isVNode === true : false
}

主要通过 __v_isVNode 属性来判断是否是 VNode,之后再判断 props 即传入的 { class: 'test'},对 classstyle 增强,这块我们放到之后来讨论。接着又对 shapeFlag 赋值,当前 typediv string 类型,此时被赋值为 ShapeFlags.ELEMENT 即等于 1,最后将处理好的 typepropschildrenshapeFlag等参数传入 createBaseVNode 方法中。

shapFlag-str.png

_createVNode 方法核心一是对 classstyle 增强,二是对 shapeFlag 标记赋值。接着我们再看下 createBaseVNode 方法:

function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag = 0,
  dynamicProps: string[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false
) {
  const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  } as VNode

  if (needFullChildrenNormalization) {
    normalizeChildren(vnode, children) // 创建子节点
    // normalize suspense children
    if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
      ;(type as typeof SuspenseImpl).normalize(vnode)
    }
  } else if (children) {
    // compiled element vnode - if children is passed, only possible types are
    // string or Array.
    vnode.shapeFlag |= isString(children)
      ? ShapeFlags.TEXT_CHILDREN
      : ShapeFlags.ARRAY_CHILDREN
  }

  // validate key
  if (__DEV__ && vnode.key !== vnode.key) {
    warn(`VNode created with invalid key (NaN). VNode type:`, vnode.type)
  }

  // track vnode for block tree
  if (
    isBlockTreeEnabled > 0 &&
    // avoid a block node from tracking itself
    !isBlockNode &&
    // has current parent block
    currentBlock &&
    // presence of a patch flag indicates this node needs patching on updates.
    // component nodes also should always be patched, because even if the
    // component doesn't need to update, it needs to persist the instance on to
    // the next vnode so that it can be properly unmounted later.
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode)
  }

  if (__COMPAT__) {
    convertLegacyVModelProps(vnode)
    defineLegacyVNodeProperties(vnode)
  }

  return vnode
}

该方法首先定义了一个 vnode 对象,属性 __v_isVNode 标记为该对象是否为 VNode 对象。由于当前 needFullChildrenNormalization 默认传入的是 true,所以直接执行 normalizeChildren(vnode, children) 方法来创建子节点,我们再来看下 normalizeChildren 方法:

export function normalizeChildren(vnode: VNode, children: unknown) {
  let type = 0
  const { shapeFlag } = vnode // 当前shapeFlag 是 1 children是字符串
  // children 为 undefined 或 null
  if (children == null) {
    children = null
  } else if (isArray(children)) { // 是否是数组
    type = ShapeFlags.ARRAY_CHILDREN
  } else if (typeof children === 'object') { // 是否是对象
    if (shapeFlag & (ShapeFlags.ELEMENT | ShapeFlags.TELEPORT)) {
      // Normalize slot to plain children for plain element and Teleport
      const slot = (children as any).default
      if (slot) {
        // _c marker is added by withCtx() indicating this is a compiled slot
        slot._c && (slot._d = false)
        normalizeChildren(vnode, slot())
        slot._c && (slot._d = true)
      }
      return
    } else {
      type = ShapeFlags.SLOTS_CHILDREN
      const slotFlag = (children as RawSlots)._
      if (!slotFlag && !(InternalObjectKey in children!)) {
        // if slots are not normalized, attach context instance
        // (compiled / normalized slots already have context)
        ;(children as RawSlots)._ctx = currentRenderingInstance
      } else if (slotFlag === SlotFlags.FORWARDED && currentRenderingInstance) {
        // a child component receives forwarded slots from the parent.
        // its slot type is determined by its parent's slot type.
        if (
          (currentRenderingInstance.slots as RawSlots)._ === SlotFlags.STABLE
        ) {
          ;(children as RawSlots)._ = SlotFlags.STABLE
        } else {
          ;(children as RawSlots)._ = SlotFlags.DYNAMIC
          vnode.patchFlag |= PatchFlags.DYNAMIC_SLOTS
        }
      }
    }
  } else if (isFunction(children)) { // 是否是 函数
    children = { default: children, _ctx: currentRenderingInstance }
    type = ShapeFlags.SLOTS_CHILDREN
  } else {
    children = String(children) // 此时 'hello render'
    // force teleport children to array so it can be moved around
    if (shapeFlag & ShapeFlags.TELEPORT) {
      type = ShapeFlags.ARRAY_CHILDREN
      children = [createTextVNode(children as string)]
    } else {
      type = ShapeFlags.TEXT_CHILDREN
    }
  }
  vnode.children = children as VNodeNormalizedChildren
  // 9 按位或赋值  vnode.shapeFlag |= type 等同于 vnode.shapeFlag = vnode.shapeFlag | type
  vnode.shapeFlag |= type 
}

该方法接收两个参数,一个是定义的 vnode 对象,一个是 childrenhello render。之后再从 vnode 对象中解构出 shapeFlag 即当前 string 类型为 1,之后根据 children 类型不同对childrentypeshapeFlag 重新赋值。由于当前 children 为 string 类型,执行 children = String(children),此时childrenhello render,并将其 vnode.children = children 重新赋值。type = ShapeFlags.TEXT_CHILDRENtype 为 8,最后对 vnode.shapeFlag |= type 按或位赋值即等于 9

shapeFlag-8.png

shapeFlag-9.png

这里拓展下 |= 按或位赋值vnode.shapeFlag |= type 等同于 vnode.shapeFlag = vnode.shapeFlag | type。当前 vnode.shapeFlag = 8type = 1,转为二进制:

// type = 1
00000000 00000000 00000000 00000001

// shapeFlag = 8
00000000 00000000 00000000 00001000

// 或 就是通过上下 ↕ 比较,如果上下是 0 则是 0,上下是 0 和 1 则是 1
// 结果是 9 
00000000 00000000 00000000 00001001

所以此时计算后的 vnode.shapeFlag = 9,之后 createBaseVNode 执行完毕返回 vnode 对象,至此 h 函数执行完毕,打印 vnode 对象:

vnode.png

我们再回过来看下 h 函数如何对 class style 增强的,该逻辑在 _createVNode 方法中:

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag: number = 0,
  dynamicProps: string[] | null = null,
  isBlockNode = false
): VNode {
  // 省略


  // class & style normalization.
  // class 和 style 的增强
  if (props) {
    // for reactive or proxy objects, we need to clone it to enable mutation.
    props = guardReactiveProps(props)! // 解析 props
    let { class: klass, style } = props // 结构  class 赋值给 klass, style
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass) // 增强 class
    }
    if (isObject(style)) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)
    }
  }

  // 省略

  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )
}

结合案例:

<!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
      // <div :class="{ red: true }">增强的 class</div>
      const vnode = h(
        'div',
        {
          class: {
            red: true
          }
        },
        '增强的 class'
      )

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

可以看出 { class: klass, style } = propsprops 解构,并将 class 赋值给 klass,如果存在 klass 且不为 string类型,则执行 props.class = normalizeClass(klass),对其 props.class 重新赋值。我们再看下 normalizeClass 方法:

export function normalizeClass(value: unknown): string {
  let res = ''
  // 是字符串 直接赋值
  if (isString(value)) {
    res = value
  } else if (isArray(value)) {
    // 是数组 则递归迭代再拼接
    for (let i = 0; i < value.length; i++) {
      const normalized = normalizeClass(value[i])
      if (normalized) {
        res += normalized + ' '
      }
    }
  } else if (isObject(value)) {
    // 是对象 则 for in 再拼接返回
    for (const name in value) {
      if (value[name]) {
        res += name + ' '
      }
    }
  }
  return res.trim()
}

该逻辑也较容易理解,根据 value 类型,如果是字符串则直接返回;如果是数组则递归迭代再拼接返回;如果是对象则迭代再拼接返回。由于当前 value 是对象 { red: true },所以此时的 resred

normalClass.png

最终结果:

class-html.png

由于 styleclass 逻辑类似,这里就不再具体展开讲解。

另外还有几种特殊的场景,比如 h 函数接收的是一个组件:

<!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 component = {
      render() {
        const vnode1 = h('div', '这是一个 component')
        console.log(vnode1)
        return vnode1
      }
    }

    const vnode2 = h(component)
    console.log(vnode2)

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

</html>

输出:

shapeFlag-comp.png

可以看出 vnode1vnode2 仅仅只是根据类型不同对 shapeFlag 赋值不同。再比如 children 参数为一个数组:

<!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 } = Vue
      // 先执行了 children h 函数 p1  
      // shapeFlag为 17 代表是 element + array children 为 9 代表是 element + text children
      const vnode = h(
        'div',
        {
          class: 'test'
        },
        [h('p', 'p1'), h('p', 'p2'), h('p', 'p3')]
      )

      console.log(vnode)
    </script>
  </body>
</html>

这里需要注意的是会优先执行 childrenh 函数,其结果也是标记 shapeFlag 不同值:

h-child.png

shapeFlag-child-array.png

最后 Vue 中还声明了 TextCommentFragment三种类型,其值为 Symbol(Text)Symbol(Comment)Symbol(Fragment)

<!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, Text, Comment, Fragment } = Vue

    const vnodeText = h(Text, '这是一个 Text')
    console.log(vnodeText)
    render(vnodeText, document.querySelector('#app'))

    const vnodeComment = h(Comment, '这是一个 Comment')
    console.log(vnodeComment)
    render(vnodeComment, document.querySelector('#app'))

    const vnodeFragment = h(Fragment, '这是一个 Fragment')
    console.log(vnodeFragment)
    render(vnodeFragment, document.querySelector('#app'))
  </script>
</body>

</html>

结果:

h-text.png

可以看出仅仅只是 type 类型不同。

总结

  1. createVNode 核心是处理 shapeFlag 赋值,之后在 createBaseVNode 中又通过 shapeFlagtype 根据按位或运算,重新对 shapeFlag 赋值。
  2. h 函数本质上是对四个属性处理 childrenpropsshapeFlagtypeclass style 的增强。

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