vnode 是怎么被创建的

2025-10-21 10:39:39

之前梳理了一下 runtime-domruntime-core 的关系

平时写的 render(vnode, app) 里,真正被传进去的是一个 vnode

vnode 是怎么来的呢

最常见的入口就是 h 函数:

const vnode = h('div', 'hello world')

render(vnode, app)

h 看起来很灵活,既能接收子节点,也能接收 props,还能接收数组子节点。它的工作其实只有一个:把不同写法统一成 createVNode 需要的参数。

h 的使用方式

第二个参数是子节点

h('div', 'hello world')
h('div', h('span', 'hello world'))
h('div', [h('span', 'hello'), h('span', 'world')])

第二个参数是 props

h('div', { class: 'container' })

第三个参数是子节点

h('div', { class: 'container' }, 'hello world')
h('div', { class: 'container' }, h('span', 'hello world'))
h('div', { class: 'container' }, [h('span', 'hello'), h('span', 'world')])

这些写法看起来很多,但最后都要落到同一种 vnode 结构上。

h 做的事情

h 本身不负责创建复杂的数据结构,它主要做的是参数归一化。

// packages/runtime-core/src/h.ts
export function h(type, propsOrChildren?, children?) {
  const l = arguments.length

  if (l === 2) {
    if (isArray(propsOrChildren)) {
      return createVNode(type, null, propsOrChildren)
    }

    if (isObject(propsOrChildren)) {
      if (isVNode(propsOrChildren)) {
        return createVNode(type, null, [propsOrChildren])
      }

      return createVNode(type, propsOrChildren, children)
    }

    return createVNode(type, null, propsOrChildren)
  }
  else {
    if (l > 3) {
      children = [...arguments].slice(2)
    }
    else if (isVNode(children)) {
      children = [children]
    }

    return createVNode(type, propsOrChildren, children)
  }
}

function isVNode(value) {
  return value?.__v_isVNode
}

当只传两个参数时,propsOrChildren 可能是:

  • 数组子节点
  • props
  • 单个 vnode
  • 普通字符串子节点

所以 h 先判断它到底是什么,再决定该把它放到 props 还是 children 里。

如果传了三个及以上参数,那就默认从第三个开始都是子节点。

createVNode

h 最终会调用 createVNode

createVNode 才是 vnode 的真正创建入口。

官方的 vnode

{
  "__v_isVNode": true,
  "type": "div",
  "props": { "style": { "color": "red" } },
  "children": [{ "__v_isVNode": true, "__v_skip": true, "type": "p", "props": null, "key": null, "ref": null, "scopeId": null, "slotScopeIds": null, "children": "hello", "component": null, "suspense": null, "ssContent": null, "ssFallback": null, "dirs": null, "transition": null, "el": null, "anchor": null, "target": null, "targetStart": null, "targetAnchor": null, "staticCount": 0, "shapeFlag": 9, "patchFlag": 0, "dynamicProps": null, "dynamicChildren": null, "appContext": null, "ctx": null }, { "__v_isVNode": true, "__v_skip": true, "type": "p", "props": null, "key": null, "ref": null, "scopeId": null, "slotScopeIds": null, "children": "world", "component": null, "suspense": null, "ssContent": null, "ssFallback": null, "dirs": null, "transition": null, "el": null, "anchor": null, "target": null, "targetStart": null, "targetAnchor": null, "staticCount": 0, "shapeFlag": 9, "patchFlag": 0, "dynamicProps": null, "dynamicChildren": null, "appContext": null, "ctx": null }],
  "key": null,
  "el": null,
  "shapeFlag": 9
}
function createVNode(type, props?, children?) {
  const vnode = {
    __v_isVNode: true,
    type,
    props,
    children,
    key: props?.key,
    el: null
  }

  return vnode
}

可以把它理解成一个很朴素的对象:

const vnode = {
  __v_isVNode: true,
  type: 'div',
  props: { class: 'container' },
  children: 'hello world',
  key: null,
  el: null
}
import { render } from '../../../node_modules/vue/dist/vue.esm-browser.js'
import { h } from '../dist/vue.esm.js'

const vnode = h('div', 'hello world')
render(vnode, app)

但经过测试后,发现并不能成功,对比官方 vnode 后, 少一个 shapeFlag, 加入 shapeFlag: 9 之后可以渲染了。

这是为什么呢 请看下一章 shapeFlag 位元算 😁