2026.01.06

h 函数 -> Vue 的虚拟节点创建机制

回顾渲染器

先回顾一下之前的代码:

import { createRenderer, h } from 'vue'
import { renderOptions } from '../dist/vue.esm.js'

const renderer = createRenderer(renderOptions)
const vnode = h('div', {
  onClick() {
    console.log('click')
  },
  id: 'node',
  a: '1'
}, 'hello world')

renderer.render(vnode, app)

我们将 DOM 操作相关的 API 放到了 runtime-dom 中,因为它的职责就是提供浏览器环境下的 DOM 操作能力。

同时,我们还用到了 createRenderer,它不能单独工作,需要配合 DOM 操作方法。

createRenderer 的返回值

createRenderer 返回一个对象,包含:

  • render:用于将虚拟节点渲染到容器中
  • createApp:用于创建 Vue 应用实例(即我们常用的 createApp(rootComponent).mount(container)

通过这种设计,Vue 实现了平台无关的渲染能力。

虚拟节点的创建

render 函数接收两个参数:vnodecontainer。那么 vnode(虚拟节点)是如何创建的呢?

h 函数的使用方式

h 函数是 Vue 中创建虚拟节点的便捷方法,它支持多种参数形式:

// ========= 两个参数 =========

// 1. 第二个参数为文本子节点
const vnode1 = h('div', 'hello world')

// 2. 第二个参数为数组子节点
const vnode2 = h('div', [h('span', 'hello'), h('span', 'world')])

// 3. 第二个参数为单个子节点
const vnode3 = h('div', h('span', 'hello'))

// 4. 第二个参数为 props
const vnode4 = h('div', { class: 'container' })

// ========= 三个参数 =========

// 5. props + 文本子节点
const vnode5 = h('div', { class: 'container' }, 'hello world')

// 6. props + 单个子节点
const vnode6 = h('div', { class: 'container' }, h('span', 'hello'))

// 7. props + 多个子节点
const vnode7 = h('div', { class: 'container' }, h('span', 'hello'), h('span', 'world'))

// 8. props + 数组子节点(效果同上)
const vnode8 = h('div', { class: 'container' }, [h('span', 'hello'), h('span', 'world')])

h 函数与 createVNode

在 Vue 中,创建虚拟节点实际上有两个函数:

  1. createVNode:真正创建虚拟节点的函数
  2. h:对 createVNode 进行参数归一化处理的便捷函数

为什么需要两个函数?

h 函数的作用是参数归一化,让我们可以用多种方式调用。从上面的示例可以看出:

  • 第二个参数可以是子节点,也可以是 props
  • 子节点可以是字符串、数组或单个虚拟节点
  • 可以传递两个参数,也可以传递三个或更多参数

这些灵活的调用方式最终都会被归一化为标准的 createVNode 调用。

h 函数的实现

import { isArray, isObject } from '@vue/shared'

/**
 * h 函数的主要作用是对 createVNode 做参数归一化
 * 支持多种调用方式,最终都转换为标准的 createVNode 调用
 */
export function h(type, propsOrChildren?, children?) {
  const l = arguments.length

  if (l === 2) {
    // 两个参数的情况
    if (isArray(propsOrChildren)) {
      // h('div', [h('span', 'hello'), h('span', 'world')])
      return createVNode(type, null, propsOrChildren)
    }

    if (isObject(propsOrChildren)) {
      if (isVNode(propsOrChildren)) {
        // h('div', h('span', 'hello'))
        return createVNode(type, null, [propsOrChildren])
      }
      // h('div', { class: 'container' })
      return createVNode(type, propsOrChildren, children)
    }

    // h('div', 'hello world')
    return createVNode(type, null, propsOrChildren)
  }
  else {
    // 三个或更多参数的情况
    if (l > 3) {
      // h('div', { class: 'container' }, h('span', 'hello'), h('span', 'world'))
      // 将第三个参数及之后的所有参数收集为 children 数组
      children = [...arguments].slice(2)
    }
    else if (isVNode(children)) {
      // h('div', { class: 'container' }, h('span', 'hello'))
      // 单个子节点也转换为数组
      children = [children]
    }

    return createVNode(type, propsOrChildren, children)
  }
}

/**
 * 判断是否为虚拟节点
 */
function isVNode(value) {
  return value?.__v_isVNode
}

createVNode 的实现

createVNode 是真正创建虚拟节点对象的函数:

/**
 * 创建虚拟节点
 * @param type 节点类型(标签名或组件)
 * @param props 节点属性
 * @param children 子节点
 */
function createVNode(type, props?, children?) {
  const vnode = {
    __v_isVNode: true, // 虚拟节点标识
    type, // 节点类型
    props, // 节点属性
    children, // 子节点
    key: props?.key, // 用于 diff 优化的 key
    el: null, // 对应的真实 DOM 元素
    shapeFlag: 9 // 节点形状标识(详见后面章节)
  }

  return vnode
}

关于 shapeFlag

在上面的代码中,我们暂时将 shapeFlag 设置为 9。这个属性用于标识虚拟节点的类型和特征,它使用位运算来存储多个值。

通过设置 shapeFlag: 9,虚拟节点可以被正确渲染。

总结

通过本章节,我们了解了:

  1. h 函数:提供灵活的参数形式,让创建虚拟节点更便捷
  2. createVNode:真正的虚拟节点创建函数,返回标准的虚拟节点对象
  3. 参数归一化:h 函数将各种参数形式统一转换为 createVNode 的标准调用
  4. 虚拟节点结构:包含 type、props、children、key、el 等核心属性

这种设计模式在 Vue 中随处可见:提供灵活的 API 给开发者,内部则使用统一的标准格式处理。