vue3源码学习11-h方法和createVnode方法实现

摘要

在实际开发中,经常用到 h 方法来实现页面,常见的组件中也有 render,h 这种写法。例如 iview 的 table 中

472cbb0b835049e7aae4f.png

那么 h 用法也有很多的多样性。例如

  • h("div",{style:{color: "black"}})
  • h("div",h('span'))
  • h('div', [h('span'),h('span)])
  • h("div","hello")
  • h("div",null,'hello','world')
  • h('div',null, h('span'))
  • h("div",{style:{color: "white"}},'hello')
  • h("div",hello)
  • h("div")

源码中 h 主要是调用 createVnode 方法创建虚拟 dom,所以主要的东西在 createVnode,h 就像一个提供方便创造的可能。

编写 createVnode

首先要明白为啥用虚拟节点而不是真实的 dom。虚拟 dom 就是一个对象,为了用于 diff 算法,真实 dom 的属性比较多。
其次虚拟节点的类型有很多,例如组件、元素、文本等。
那么为了判断虚拟 dom 的类型,需要有一个判断类型的方法。
在 shared 编写一个 ShapeFlags 的函数。

1
2
3
4
5
6
7
8
9
10
11
12
export const enum ShapeFlags {
ELEMENT = 1, // HTML 或 SVG 标签 普通 DOM 元素
FUNCTIONAL_COMPONENT = 1 << 1, // 函数式组件
STATEFUL_COMPONENT = 1 << 2, // 普通有状态组件
TEXT_CHILDREN = 1 << 3, // 子节点为纯文本
ARRAY_CHILDREN = 1 << 4, // 子节点是数组
SLOTS_CHILDREN = 1 << 5, // 子节点是插槽
TELEPORT = 1 << 6, // Teleport
SUSPENSE = 1 << 7, // Supspense
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 需要被keep-live的有状态组件
COMPONENT_KEPT_ALIVE = 1 << 9, //已经被keep-live的有状态组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT, // 有状态组件和函数组件都是组件,用component表示

这样就有了一个判断传入的孩子节点的类型判断方法了。
为了统一代码的编写,确认孩子的类型,将孩子放在一个数组中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { isArray, isString, ShapeFlags } from "@vue/shared";

// 虚拟节点有很多: 组件, 元素的、 文本的
// 先写元素
export function createVnode(type, props, children = null) {
let shapeFlag = isString(type) ? ShapeFlags.ELEMENT : 0;

// 虚拟dom就是一个对象,为了用于diff算法,真实dom的属性比较多
const vnode = {
// key 虚拟节点的标识
type,
props,
children,
el: null, // 虚拟节点对应的真实节点。后续diff算法
key: props?.["key"],
__v_isVnode: true,
shapeFlag,
};

if (children) {
let type = 0;
if (isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN;
} else {
children = String(children);
type = ShapeFlags.TEXT_CHILDREN;
}
vnode.shapeFlag |= type;
}

return vnode;
}

export function isVnode(value) {
return !!(value && value.__v_isVnode);
}

有了上面创建虚拟节点的方法之后,h 就是一个对写法的支持划分了。
按照上面 h 可以有的写法。编写自己的 h 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// h 的用法
// h("div")
// h("div",hello)
// h("div",{style:{color: "white"}},'hello')

import { isArray, isObject } from "@vue/shared";
import { createVnode, isVnode } from "./vnode";

// h("div",null,'hello','world')
// h('div',null, h('span'))

export function h(type, propsChildren?: any, children?: any) {
// 其余的除了3个之外的肯定都是孩子
const l = arguments.length;

// h("div",{style:{color: "black"}})
// h("div",h('span'))
// h('div', [h('span'),h('span)])
// h("div","hello")
if (l === 2) {
// 为什么要将儿子包装成数组,因为元素可以循环创建。 文本不需要包装了
if (isObject(propsChildren) && !isArray(propsChildren)) {
// 虚拟节点就包装成数组
if (isVnode(propsChildren)) {
return createVnode(type, null, [propsChildren]);
}
return createVnode(type, propsChildren); // 属性
} else {
return createVnode(type, null, propsChildren); // 是数组
}
} else {
if (l > 3) {
children = Array.from(arguments).slice(2);
} else if (l === 3 && isVnode(children)) {
children = [children];
}
return createVnode(type, propsChildren, children);
// children的情况有2中 文本 / 数组
}
}

在上面判断传入参数的个数来区分。同时如果就一个孩子,那么为了写法处理的统一,放入到数组中。
所以最后就 2 种情况

  • h("h1",{},[])
  • h("h1",null, 文本)

bookmark