用位运算标记 vnode 的类型和 children 类型
上一节实现了基础的 createVNode。
但测试时会发现,只创建出 type、props、children 这些字段还不够。
如果缺少 shapeFlag,渲染流程会出现类似这样的警告:
[Vue warn]: Invalid VNode type: div (string)
而当我们给 vnode 补上 shapeFlag: 9 后,内容就可以正常渲染了。
那这个 shapeFlag 到底是什么?
shapeFlag 是什么
shapeFlag 可以理解成 vnode 的类型标记。
记录了两个重要信息:
- 当前
vnode自己是什么类型 - 当前
vnode的 children 是什么类型
const vnode = h('div', 'hello world')
比如这个 vnode:它包含两层信息:
type是字符串'div',说明它是普通 DOM 元素children是字符串'hello world',说明它的子节点是文本
如果每次渲染时都重新判断 typeof vnode.type、typeof vnode.children,当然也能做。
但 Vue 会在创建 vnode 的时候就把这些信息编码到 shapeFlag 里,后面渲染时直接判断标记。
可以把它类比成身份证。
身份证号码不是一个普通随机数,不同位置本身就带有含义,比如地区、出生日期、顺序码。
shapeFlag 也是类似的思路:一个数字里可以同时存放多个标记。
ShapeFlags
Vue 里会用枚举来定义这些标记:
export enum ShapeFlags {
// DOM 元素
ELEMENT = 1,
// 函数组件
FUNCTIONAL_COMPONENT = 1 << 1,
// 有状态组件
STATEFUL_COMPONENT = 1 << 2,
// 子节点是文本
TEXT_CHILDREN = 1 << 3,
// 子节点是数组
ARRAY_CHILDREN = 1 << 4,
// 子节点是插槽
SLOTS_CHILDREN = 1 << 5,
// Teleport
TELEPORT = 1 << 6,
// Suspense
SUSPENSE = 1 << 7,
// 组件应该被 keep-alive
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
// 组件已经被 keep-alive
COMPONENT_KEPT_ALIVE = 1 << 9,
// 组件类型
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
这里的 1 << n 是左移运算。
它会让每个标记占据不同的二进制位:
ShapeFlags.ELEMENT // 0001
ShapeFlags.TEXT_CHILDREN // 1000
ShapeFlags.ARRAY_CHILDREN // 10000
这样不同标记之间不会互相覆盖,就可以放进同一个数字里。
为什么 shapeFlag 是 9
const vnode = h('div', 'hello world')
它是一个普通元素,并且子节点是文本。
所以它需要同时具备两个标记:
ShapeFlags.ELEMENT // 1
ShapeFlags.TEXT_CHILDREN // 8
用二进制表示就是:
0001
1000
通过或运算 | 合并:
0001
1000
----
1001
1001 转成十进制就是 9。
所以 shapeFlag: 9 的意思不是一个神秘数字,它表示:
- 这是一个元素节点
- 它的 children 是文本
创建 shapeFlag
在 createVNode 里,可以先根据 type 得到基础标记。
let shapeFlag = 0
if (typeof type === 'string') {
shapeFlag = ShapeFlags.ELEMENT
}
然后再根据 children 的类型,把对应标记合并进去:
if (typeof children === 'string') {
shapeFlag = shapeFlag | ShapeFlags.TEXT_CHILDREN
}
else if (Array.isArray(children)) {
shapeFlag = shapeFlag | ShapeFlags.ARRAY_CHILDREN
}
最终放到 vnode 上:
function createVNode(type, props?, children?) {
const vnode = {
__v_isVNode: true,
type,
props,
children,
key: props?.key,
el: null,
shapeFlag: 0
}
if (typeof type === 'string') {
vnode.shapeFlag |= ShapeFlags.ELEMENT
}
if (typeof children === 'string') {
vnode.shapeFlag |= ShapeFlags.TEXT_CHILDREN
}
else if (Array.isArray(children)) {
vnode.shapeFlag |= ShapeFlags.ARRAY_CHILDREN
}
return vnode
}
|=等于vnode.shapeFlag = vnode.shapeFlag | ShapeFlags.TEXT_CHILDREN
判断 shapeFlag
有了 shapeFlag 后,渲染器就可以通过与运算 & 判断 vnode 是否包含某个标记。
if (vnode.shapeFlag & ShapeFlags.ELEMENT) {
console.log('这是一个 DOM 元素')
}
if (vnode.shapeFlag & ShapeFlags.TEXT_CHILDREN) {
console.log('子节点是文本')
}