createElement → VNode 是怎么创建的
一、createElement 在哪里?
真正核心函数在:src/core/vdom/create-element.js
initRender 里安装的是:
js
vm._c = (...) => createElement(vm, ...)
vm.$createElement = (...) => createElement(vm, ...)
二、createElement 的函数签名
简化版:
js
function createElement(
context,
tag,
data,
children,
normalizationType,
alwaysNormalize
)
参数解释:
| 参数 | 作用 |
|---|---|
| context | 当前组件实例 |
| tag | 标签名 或 组件 |
| data | attrs、props、on 等 |
| children | 子节点 |
| normalizationType | 子节点规范化类型 |
| alwaysNormalize | 是否强制标准化 |
三、createElement 内部流程(核心逻辑)
流程图看全貌:
Plain
createElement
│
├─ 参数兼容处理
│
├─ children 规范化
│
├─ 判断 tag 类型
│ ├─ 字符串
│ │ ├─ 原生标签 → 创建普通 VNode
│ │ └─ 组件名 → 创建组件 VNode
│ │
│ └─ 构造函数 → 创建组件 VNode
│
└─ 返回 VNode
四、第一步:参数处理
Vue 允许写:
js
h('div', [child])
也允许:
js
h('div', data, children)
内部会做:
js
if (Array.isArray(data) || isPrimitive(data)) {
children = data
data = undefined
}
参数兼容处理。
五、第二步:children 规范化
Vue 内部要求:
children 必须是 VNode 数组
但可能写:
js
h('div', 'hello')
所以要统一处理:
js
children = normalizeChildren(children)
规范化会做:
- 把字符串变成文本 VNode
- 把嵌套数组拍平
- 合并相邻文本节点
六、第三步:判断 tag 类型(关键)
js
if (typeof tag === 'string') {
分三种情况:
1️⃣ 原生标签
js
h('div')
判断方式:
js
if (config.isReservedTag(tag))
例如:
- div
- span
- p
会创建普通 VNode:
js
return new VNode(tag, data, children, ...)
2️⃣ 组件
js
h(MyComponent)
或者:
js
h('my-component')
Vue 会通过:
js
resolveAsset(context.$options, 'components', tag)
找到组件构造函数。
然后进入:
js
createComponent(...)
🔥 createComponent 是关键
这个函数会创建一个:
js
componentVNode
并在 data.hook 上挂载:
js
init
prepatch
insert
destroy
这些钩子会在 patch 阶段触发。
七、VNode 长什么样?
VNode 本质是一个类:
js
class VNode {
constructor(
tag,
data,
children,
text,
elm,
context,
componentOptions
)
}
一个普通 VNode 结构大概是:
js
{
tag: 'div',
data: { attrs: {}, on: {} },
children: [VNode, VNode],
text: undefined,
elm: undefined,
context: vm,
componentOptions: undefined
}
组件 VNode 则多了:
js
componentOptions: {
Ctor,
propsData,
listeners,
children
}
八、普通元素 vs 组件的区别
| 类型 | patch 时行为 |
|---|---|
| 普通 VNode | 创建真实 DOM |
| 组件 VNode | 创建组件实例 |
这就是:
Plain
VNode 只是描述
patch 才是执行
九、完整链路串起来
Plain
模板
↓
编译成 render 函数
↓
render 调用 _c
↓
createElement
↓
创建 VNode
↓
patch
↓
根据 VNode 类型
├─ 创建 DOM
└─ 创建组件实例
十、举个完整例子
模板:
vue
<div>
<child />
</div>
编译后:
js
render() {
return _c('div', [
_c('child')
])
}
执行顺序:
- createElement('div') → 普通 VNode
- createElement('child') → 组件 VNode
- patch
- 创建 div DOM
- 发现 child 是组件 → new Child()
- 子组件再 render
十一、核心理解
一句话:
createElement 只负责"描述",patch 才负责"执行"。
VNode 是:
Plain
DOM 的蓝图
patch 是:
Plain
施工队