实现自己的virtual dom

通过实现一个短小的 virtual dom,了解 virtual dom 的原理,进而加深对 React 的理解。项目地址

今天要实现的功能如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//定义一个组件
let MyButton = {
render: ({ props, children }) => (
<button onClick={addCount} {...props}>
{children}
{count}
</button>
)
};
let count = 0;
let addCount = () => {
count++;
app();
};
//渲染函数,用来生成dom
let render = createApp(document.body);

let app = () => {
render(<MyButton className="button">hello, button</MyButton>);
};

app();

看起来是不是很像 react?

  1. 支持自定义组价(组件可互相嵌套)
  2. 事件
  3. 支持 attribute
  4. 组件的 diff 更新

下面我们来一步步实现这些功能。

使用过 jsx 的同学对它肯定很喜欢

1
2
3
4
5
6
const wrapper = (
<div className="wrapper">
<span>hello</span>
<span> world!</span>
</div>
);

如果想使用 jsx 语法,我们可以借助于 babel。babel 在解析 jsx 的时候需要指定一个函数,如果你写过 react 的话,那默认是 React.createElement 函数。这里我们这里定义一个函数 create:

1
2
3
4
5
6
7
function create(type, attributes, ...children) {
return {
type,
props: attributes,
children
};
}

有个这个函数 wrapper, babel 在解析 wrapper 的时候会依次传入 type、attributes、child1、child2….

下边的写法完全等价于上边的 jsx 语法:

1
2
3
4
5
6
const wrapper = create(
"div",
{ className: "wrapper" },
create("span", {}, "hello"),
create("span", {}, "world!")
);

创建

现在我们需要把 wrapper 转换为真正的 dom,我们定义一个方法 createElement:

1
2
3
4
5
6
7
8
/***
* 将vnode转换为dom
* @param {Object} vnode - 虚拟dom
* @return {Node} - dom
*/
function createElement(vnode) {
return document.createElement(vnode.type);
}

createElement 方法会将 vnode 转换为真实的 dom,但是目前这个方法只能处理 Element 类型的元素,文本元素都无法处理。我们先看下有多少个类型需要处理。

  1. Element 类型(native)
  2. Text 类型(text)
  3. 自定义组件类型, 如 MyButton(thunk)
  4. 空类型(empty)在 react 中会生成 noscript。{undefined}就会生成一个空类型,空类型的作用在与站位,便于 diff 算法优化。

所以我们来改写下 createElement:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/***
* 将vnode转换为dom
* @param {Object} vnode - 虚拟dom
* @return {Node} - dom
*/
function createElement(vnode) {
switch (vnode.type) {
case "text":
//文本类型
return createTextNode(vnode.nodeValue);
case "thunk":
//自定义组件类型
return createThunk(vnode);
case "empty":
//空类型
return createEmptyHTMLElement();
case "native":
return createHTMLElement(vnode);
}
}

写到这里,可能有些人会不知道 vnode 和 vnode 的 type 属性从哪里来的,这里我们回过头来重写下 create 函数:

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
41
42
43
44
45
46
47
function create(type, attributes, ...children){
f(!type) throw new TypeError('element() needs a type.')
attributes = attributes || {}
//处理子元素, 包括对Text类型的处理
children = Array.prototype.reduce.call(children || [], reduceChildren, [])

//处理自定义组件
if(typeof type === 'object'){

return return {
type: 'thunk',
fn: type.fn,
props:attributes,
children
}
}

//处理Element类型
return {
type: 'native',
tagName: type,
attributes,
children,
}
}

/**
* 处理子元素
* @param {Array} children=[]
* @param {Object} vnode
* @return {Array} children 经过转换的vnode数组
*/
function reduceChildren(children, vnode){
if(isString(vnode) || isNumber(vnode)) {
//{type: 'text'}
children.push(createTextElement(vnode))
}else if(isNull(vnode) || isUndefined(vnode)){
children.push(createEmptyElement())
}else if(Array.isArray(vnode)){
//处理{children}
children = [...children, ...vnode.reduce(reduceChildren, [])]
}else {
children.push(vnode)
}

return children
}

更新

dom 创建完成之后,就需要更新 dom 操作了。其实 dom 的更新可以抽象出一下几点

  1. 添加(appendChild),增加了新的元素
  2. 删除 (removeChild),删除了元素
  3. 取代 (replaceChild),元素类型不同时元素取代
  4. 更新(diffAttribute, diffChildren),元素类型相同,则比较 attribute 和 children
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
41
42
43
44
45
46
47
48
49
50
51
/**
* 更新node
* @param node -dom node, parent node of vdom
* @param pre -pre vnode
* @param next -next vnode
* @param index - child index in parent
* @returns node
*/
export function updateElement(node, pre, next, index = 0) {
if (pre === next) return node;

if (!isUndefined(pre) && isUndefined(next)) {
return removeNode(node, pre, next, index);
}

if (isUndefined(pre) && !isUndefined(next)) {
node.appendChild(createElement(next));
return node;
}

if ((!isNull(pre) && isNull(next)) || (isNull(pre) && !isNull(next))) {
return replaceNode(node, pre, next, index);
}

if (pre.type !== next.type) {
return replaceNode(node, pre, next, index);
}

if (isNative(next)) {
if (pre.tagName !== next.tagName) {
return replaceNode(node, pre, next, index);
}
diffAttributes(node, pre, next, index);
return diffChildren(node, pre, next, index);
}

if (isText(next)) {
if (pre.nodeValue !== next.nodeValue) {
node.childNodes[index].nodeValue = next.nodeValue;
}
return node;
}

if (isThunk(next)) {
if (isSameThunk(pre, next)) {
return updateThunk(node, pre, next, index);
} else {
return replaceThunk(node, pre, next, index);
}
}
}

文章中的代码大多参考了这里: