render
函数返回结果就是 h
函数执行的结果,因此 h
函数的输出为 VNode
。
所以需要先设计一下我们的 VNode
。
一个 html
标签有它的标签名、属性、事件、样式、子节点等诸多信息,这些内容都需要在 VNode
中体现。
<div id="div">
div text
<p>p text</p>
</div>
const elementVNode = {
tag: 'div',
props: {
id: 'div'
},
text: 'div text',
children: [{
tag: 'p',
props: null,
text: 'p text'
}]
}
上面的代码显示了 DOM 变成 VNode
的表现形式,VNode
各属性解释:
tag
:表示 DOM 元素的标签名,如div
、span
等props
:表示 DOM 元素上的属性,如id
、class
等children
:表示 DOM 元素的子节点text
:表示 DOM 元素的文本节点
这样设计 VNode
完全没有问题(实际上 Vue 2 就是这样设计的),但是 Vue 3 设计的 VNode
并不包含 text
属性,而是直接用 children
代替,因为 text
本质也是 DOM 的子节点。
在保证语义讲得通的情况下尽可能复用属性,可以使 VNode
对象更加轻量。
基于此我们把刚才的 VNode
修改成如下形式:
const elementVNode = {
tag: 'div',
props: {
id: 'div'
},
children: [{
tag: null,
props: null,
children: 'div text'
}, {
tag: 'p',
props: null,
children: 'p text'
}]
}
什么是抽象内容呢?组件就属于抽象内容,比如下面这一段模板内容:
<div>
<MyComponent></MyComponent>
</div>
MyComponent
是一个组件,我们预期渲染出 MyComponent
组件所有的内容,而不是一个 MyComponent
标签,这用 VNode
如何表示呢?
上一段内容我们其实已经通过 tag
是否为 null
来区分元素节点和文本节点了,那这里我们可以通过 tag
是否是字符串判断是标签还是组件呢?
const elementVNode = {
tag: 'div',
props: null,
children: [{
tag: MyComponent,
props: null
}]
}
理论上是可以的,Vue 2 中就是通过 tag
来判断的,具体过程如下,可以在这里看源码:
VNode.tag
如果不是字符串,则创建组件类型的VNode
VNode.tag
是字符串- 若是内置的
html
或svg
标签,则创建正常的VNode
- 若是属于某个组件的 id,则创建组件类型的
VNode
- 未知或没有命名空间的组件,直接创建
VNode
- 若是内置的
以上这些判断都是在挂载(或 patch
)阶段进行的,换句话说,一个 VNode
表示的内容需要在代码运行阶段才知道。这就带来了两个难题:无法从 AOT
的层面优化、开发者无法手动优化。
如果可以提前知道 VNode
类型,那么就可以对其进行优化,所以这里我们可以定义好一套用来判断 VNode
类型的规则,随便是用 FLAG = 1
这样的数字表示还是其它方法。
这里我们给 VNode
增加一个字段 shapeFlag
(这是为了和 Vue 3 保持一致),它是一个枚举类型变量,具体如下:
export const enum ShapeFlags {
// html 或 svg 标签
ELEMENT = 1,
// 函数式组件
FUNCTIONAL_COMPONENT = 1 << 1,
// 普通有状态组件
STATEFUL_COMPONENT = 1 << 2,
// 子节点是纯文本
TEXT_CHILDREN = 1 << 3,
// 子节点是数组
ARRAY_CHILDREN = 1 << 4,
// 子节点是 slots
SLOTS_CHILDREN = 1 << 5,
// Portal
PORTAL = 1 << 6,
// Suspense
SUSPENSE = 1 << 7,
// 需要被keepAlive的有状态组件
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
// 已经被keepAlive的有状态组件
COMPONENT_KEPT_ALIVE = 1 << 9,
// 有状态组件和函数式组件都是“组件”,用 COMPONENT 表示
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
现在我们可以修改我们的 VNode
如下:
const elementVNode = {
shapeFlag: ShapeFlags.ELEMENT,
tag: 'div',
props: null,
children: [{
shapeFlag: ShapeFlags.COMPONENT,
tag: MyComponent,
props: null
}]
}
shapeFlag
如何用来判断 VNode
类型呢?按位运算即可。
const isComponent = vnode.shapeFlag & ShapeFlags.COMPONENT
熟悉一下按位运算。
a & b
:对于每一个比特位,只有两个操作数相应的比特位都是1时,结果才为1,否则为0。a | b
:对于每一个比特位,当两个操作数相应的比特位至少有一个1时,结果为1,否则为0。
我们把 ShapeFlags
对应的值列出来,如下:
ShapeFlags | 操作 | bitmap |
---|---|---|
ELEMENT | 0000000001 |
|
FUNCTIONAL_COMPONENT | 1 << 1 | 000000001 0 |
STATEFUL_COMPONENT | 1 << 2 | 00000001 00 |
TEXT_CHILDREN | 1 << 3 | 0000001 000 |
ARRAY_CHILDREN | 1 << 4 | 000001 0000 |
SLOTS_CHILDREN | 1 << 5 | 00001 00000 |
PORTAL | 1 << 6 | 0001 000000 |
SUSPENSE | 1 << 7 | 001 0000000 |
COMPONENT_SHOULD_KEEP_ALIVE | 1 << 8 | 01 00000000 |
COMPONENT_KEPT_ALIVE | 1 << 9 | 1 000000000 |
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
根据上表展示的基本 flags
值可以很容易地得出下表:
ShapeFlags | bitmap |
---|---|
COMPONENT | 00000001 1 0 |
上面我们已经看到了 children
可以是数组或纯文本,但真实场景可能是:
null
- 纯文本
- 数组
这里我们可以增加一个 ChildrenShapeFlags
的变量表示 children
的类型,但是基于之前的设计原则,我们完全可以用 ShapeFlags
来表示,那么同一个 ShapeFlags
如何既用来表示 VNode
的类型,又用来表示其 children
的类型呢?
仍然是按位运算,我们通过 JavaScript 代码判断 children
类型,然后和当前 VNode
进行按位或运算即可。
我们增加如下函数用来专门处理子节点类型,这和 Vue 3 中的处理一致:
function normalizeChildren(vnode, children) {
let type = 0
if (children == null) {
children = null
} else if (Array.isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN
} else if (typeof children === 'string') {
children = String(children)
type = ShapeFlags.TEXT_CHILDREN
}
vnode.shapeFlag |= type
}
这样我们就可以直接通过 shapeFlag
同时判断 VNode
及其 children
类型了。
为什么
children
也需要标识呢?原因只有一个:为了patch
过程的优化。
至此,我们可以定义 VNode
结构如下:
export interface VNodeProps {
[key: string]: any
}
export interface VNode {
// _isVNode 是 VNode 对象
_isVNode: true
// el VNode 对应的真实 DOM
el: Element | null
shapeFlag: ShapeFlags.ELEMENT,
tag: | string | Component | null,
props: VNodeProps | null,
children: string | Array<VNode>
}
实际上,Vue 3 中对 VNode
的定义要复杂的多,这里就不去细看了。
消化一下,下回继续~