Vue3源码解析之 h

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

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

前言

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

案例

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

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

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

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

h 实现

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

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

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

ts 复制代码
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 方法中。

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

ts 复制代码
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 方法:

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

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

ts 复制代码
// 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 对象:

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

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

结合案例:

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

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

最终结果:

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

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

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

输出:

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

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 } = 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 不同值:

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

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

结果:

可以看出仅仅只是 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
相关推荐
也无晴也无风雨37 分钟前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang1 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、4 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤5 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js
别忘了微笑_cuicui6 小时前
elementUI中2个日期组件实现开始时间、结束时间(禁用日期面板、控制开始时间不能超过结束时间的时分秒)实现方案
前端·javascript·elementui