vnode 是怎么被创建的
2025-10-21 10:39:39
之前梳理了一下 runtime-dom 和 runtime-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 位元算 😁