virtualdom

# 渲染 DOM
经历过 PHP 模板开发或者 JQuery 的洗礼的人都知道,它们实现重新渲染采用最简单粗暴的办法就是重新构建 DOM 替换旧 DOM,问题也很明显:
- 性能消耗高
- 无法保存状态(聚焦,滚动等)
我们先看看创建一个元素所包含的实例属性有多少个
const div = document.createElement("div");
let num = 0;
for (let k in div) {
num++;
}
console.log(num); // 241
2
3
4
5
6
然后浏览器根据 CSS 规则查找匹配节点,计算合并样式布局,为了避免重新计算一般浏览器会保存这些数据.但这是整个过程下来依然会耗费大量的内存和 CPU 资源
# Virtual DOM
实际也是操作 Dom 树进行渲染更新,但是它只是针对修改部分进行局部渲染,将影响降到最低,虽然实现方式各有不同,但是大体步骤如下:
- 用 js 对象描述 DOM 树结构,用它来构建真正的 DOM 树插入文档
- 当状态改变后,重新构造新的 js 对象结构和旧的做对比得出差异
- 针对差异之处重新构建更新视图
无非就是利用 Js 做一层映射比较,操作简单并且速度远远高于直接比较 Dom 树
# 基础工具函数
无非就是一些类型判断,循环遍历的简化函数
function type(obj) {
return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, "");
}
function isArray(list) {
return type(list) === "Array";
}
function isObject(obj) {
return type(obj) === "Object";
}
function isString(str) {
return type(str) === "String";
}
function isNotEmptyObj(obj) {
return isObject(obj) && JSON.stringify(obj) != "{}";
}
function objForEach(obj, fn) {
isNotEmptyObj(obj) && Object.keys(obj).forEach(fn);
}
function aryForEach(ary, fn) {
ary.length && ary.forEach(fn);
}
function setAttr(node, key, value) {
switch (key) {
case "style":
node.style.cssText = value;
break;
case "value":
var tagName = node.tagName || "";
tagName = tagName.toLowerCase();
if (tagName === "input" || tagName === "textarea") {
node.value = value;
} else {
// if it is not a input or textarea, use `setAttribute` to set
node.setAttribute(key, value);
}
break;
default:
node.setAttribute(key, value);
break;
}
}
function toArray(data) {
if (!data) {
return [];
}
const ary = [];
aryForEach(data, (item) => {
ary.push(item);
});
return ary;
}
export {
isArray,
isObject,
isString,
isNotEmptyObj,
objForEach,
aryForEach,
setAttr,
toArray,
};
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# Javascript 对象结构描述
<div className="num" index={1}>
<span>123456</span>
</div>
2
3
"use strict";
React.createElement(
"div",
{
className: "num",
index: 1,
},
React.createElement("span", null, "123456")
);
2
3
4
5
6
7
8
9
10
创建一个 Element 类负责将 Javascript 对象结构转换为 Dom 树结构
import {
isObject,
isString,
isArray,
isNotEmptyObj,
objForEach,
aryForEach,
} from "./util";
import { NOKEY } from "./common";
class Element {
constructor(tagName, props, children) {
// 解析参数
this.tagName = tagName;
// 字段处理,可省略参数
this.props = isObject(props) ? props : {};
this.children =
children ||
(!isNotEmptyObj(this.props) &&
((isString(props) && [props]) || (isArray(props) && props))) ||
[];
// 无论void后的表达式是什么,void操作符都会返回undefined
this.key = props ? props.key : void NOKEY;
// 计算节点数
let count = 0;
aryForEach(this.children, (item, index) => {
if (item instanceof Element) {
count += item.count;
} else {
this.children[index] = "" + item;
}
count++;
});
this.count = count;
}
render() {
// 根据tagName构建
const dom = document.createElement(this.tagName);
// 设置props
objForEach(this.props, (propName) =>
dom.setAttribute(propName, this.props[propName])
);
// 渲染children
aryForEach(this.children, (child) => {
const childDom =
child instanceof Element
? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
: document.createTextNode(child); // 如果字符串,只构建文本节点
dom.appendChild(childDom);
});
return dom;
}
}
// 改变传参方式,免去手动实例化
export default function CreateElement(tagName, props, children) {
return new Element(tagName, props, children);
}
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
52
53
54
55
56
57
58
59
60
61
62
调用如下:
// 1. 构建虚拟DOM
const tree = createElement("div", { id: "root" }, [
createElement("h1", { style: "color: blue" }, ["Tittle1"]),
createElement("p", ["Hello, virtual-dom"]),
createElement("ul", [
createElement("li", { key: 1 }, ["li1"]),
createElement("li", { key: 2 }, ["li2"]),
createElement("li", { key: 3 }, ["li3"]),
createElement("li", { key: 4 }, ["li4"])
])
]);
// 2. 通过虚拟DOM构建真正的DOM
const root = tree.render()
document.body.appendChild(root)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
运行结果如下:
js结构如图:
结构原型如下:
# diff算法
是整个实现中最关键的一步,因为这决定了计算的速度和操作DOM的数量 创建新的DOM树作对比
// 3. 生成新的虚拟DOM
const newTree = createElement("div", { id: "container" }, [
createElement("h1", { style: "color: red" }, ["Title2"]),
createElement("h3", ["Hello, virtual-dom"]),
createElement("ul", [
createElement("li", { key: 3 }, ["li3"]),
createElement("li", { key: 1 }, ["li1"]),
createElement("li", { key: 2 }, ["li2"]),
createElement("li", { key: 5 }, ["li5"])
])
]);
2
3
4
5
6
7
8
9
10
11
js结构如图:
# tree diff
传统diff算法的复杂度为O(n^3),但一般DOM跨层级的情况是非常少见的。
所以React只针对同层级DOM节点做比较,将O(n^3)复杂度的问题转换成O(n)复杂度的问题。
比较大的问题是当节点跨级移动并不会进行移动而是直接替换整个节点,所以性能问题一般出在这个地方。
# component diff
- 某组件发生变化,会导致自其从上往下整体替换
- 同一类型组件会进行Virtual DOM 比较
- React 提供一个
shouldComponentUpdate决定是否更新 尽可能将动态组件往底层节点迁移,有利于提高性能
# element diff
元素操作无非就几种,定义几个类型做状态标记
const REPLACE = "replace";
const REORDER = "reorder";
const PROPS = "props";
const TEXT = "text";
const NOKEY = "no_key"
export {
REPLACE,
REORDER,
PROPS,
TEXT,
NOKEY
}
2
3
4
5
6
7
8
9
10
11
12
13
其中NOKEY是专门给那些没有定义key的组件做默认,React对同一层级的同组子节点,添加唯一key进行区分位移而不是直接替换,这点对整体性能尤为关键。 针对不同类型做区分处理
import { isString, objForEach, aryForEach, isNotEmptyObj } from "./util";
import { REPLACE, REORDER, PROPS, TEXT } from "./common";
import listDiff from "list-diff2";
/**
*
* @param {旧Dom树} oTree
* @param {新Dom树} nTree
* 返回差异记录
*/
function diff(oTree, nTree) {
// 节点位置
let index = 0;
// 差异记录
const patches = {};
dfsWalk(oTree, nTree, index, patches);
return patches;
}
function dfsWalk(oNode, nNode, index, patches) {
const currentPatch = [];
// 首次渲染
if (nNode === null) return;
// 都是字符串形式并且不相同直接替换文字
if (isString(oNode) && isString(nNode)) {
oNode !== nNode &&
currentPatch.push({
type: TEXT,
content: nNode
});
// 同种标签并且key相同
} else if (oNode.tagName === nNode.tagName && oNode.key === nNode.key) {
// 至少一方有值
if (isNotEmptyObj(oNode.props) || isNotEmptyObj(nNode.props)) {
// 计算props结果
const propsPatches = diffProps(oNode, nNode);
// 有差异则重新排序
propsPatches &&
currentPatch.push({
type: PROPS,
props: propsPatches
});
}
// children对比
if (
!(!isNotEmptyObj(nNode.props) && nNode.props.hasOwnProperty("ignore"))
) {
(oNode.children.length || nNode.children.length) &&
diffChildren(
oNode.children,
nNode.children,
index,
patches,
currentPatch
);
}
} else {
// 都不符合上面情况就直接替换
currentPatch.push({ type: REPLACE, node: nNode });
}
// 最终对比结果
currentPatch.length && (patches[index] = currentPatch);
}
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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
新旧节点的props属性比较
/**
*
* @param {旧节点} oNode
* @param {新节点} nNode
*/
function diffProps(oNode, nNode) {
let isChange = false;
const oProps = oNode.props;
const nProps = nNode.props;
// 节点属性记录
const propsPatched = {};
// 替换/新增属性
objForEach(oProps, key => {
if (nProps[key] !== oProps[key] || !oProps.hasOwnProperty(key)) {
!isChange && (isChange = true);
propsPatched[key] = nProps[key];
}
});
return !isChange ? null : propsPatched;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
新旧节点子元素对比
/**
* 同级对比
* @param {*} oChildren
* @param {*} nChildren
* @param {*} index
* @param {*} patches
* @param {*} currentPatch
*/
function diffChildren(oChildren, nChildren, index, patches, currentPatch) {
// 得出相对简化移动路径
const diffs = listDiff(oChildren, nChildren, "key");
// 保留元素
nChildren = diffs.children;
// 记录排序位移
diffs.moves.length &&
currentPatch.push({ type: REORDER, moves: diffs.moves });
// 深度遍历
let leftNode = null;
let currentNodeIndex = index;
aryForEach(oChildren, (_item, _index) => {
const nChild = nChildren[_index];
currentNodeIndex =
leftNode && leftNode.count
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1;
_item !== nChild && dfsWalk(_item, nChild, currentNodeIndex, patches);
leftNode = _item;
});
}
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
深度遍历的原型图如下:
调用对比方式
// 4. 比较两棵虚拟DOM树的不同
const patches = diff(tree, newTree);
2
得出差异如下:
# 更新视图
进行深度遍历
import {
isString,
isObject,
objForEach,
aryForEach,
setAttr,
toArray
} from "./util";
import { REPLACE, REORDER, PROPS, TEXT, NOKEY } from "./common";
function patch(node, patches) {
const walker = { index: 0 };
dfsWalk(node, walker, patches);
}
// 深度遍历更新
function dfsWalk(node, walker, patches) {
const currentPatches = patches[walker.index];
node.childNodes &&
aryForEach(node.childNodes, item => {
walker.index++;
dfsWalk(item, walker, patches);
});
currentPatches && applyPatches(node, currentPatches);
}
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
针对不同标志做对应处理
// 更新类型
function applyPatches(node, currentPatches) {
aryForEach(currentPatches, item => {
switch (item.type) {
case REPLACE:
const nNode = isString(item.node)
? document.createTextNode(item.node)
: item.node.render();
node.parentNode.replaceChild(nNode, node);
break;
case REORDER:
reorderChildren(node, item.moves);
break;
case PROPS:
setProps(node, item.props);
break;
case TEXT:
if (node.textContent) {
// 使用纯文本
node.textContent = item.content;
} else {
// 仅仅对CDATA片段,注释comment,Processing Instruction节点或text节点有效
node.nodeValue = item.content;
}
break;
default:
throw new Error("Unknown patch type " + item.type);
}
});
}
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
属性替换
// 修改属性
function setProps(node, props) {
objForEach(props, key => {
if (props[key] === void NOKEY) {
node.removeAttribute(key);
} else {
setAttr(node, key, props[key]);
}
});
}
2
3
4
5
6
7
8
9
10
列表渲染
// 列表排序渲染
function reorderChildren(node, moves) {
const staticNodeList = toArray(node.childNodes);
const maps = {};
aryForEach(staticNodeList, node => {
// Element
if (node.nodeType === 1) {
const key = node.getAttribute("key");
key && (maps[key] = node);
}
});
aryForEach(moves, move => {
const index = move.index;
// 0:删除 1:替换
if (move.type === 0) {
// 找到对应节点删除
staticNodeList[index] === node.childNodes[index] &&
node.removeChild(node.childNodes[index]);
staticNodeList.splice(index, 1);
} else if (move.type === 1) {
let insertNode;
if (maps[move.item.key]) {
// 删除并返回节点
insertNode = node.removeChild(maps[move.item.key]);
// 获取删除节点位置
staticNodeList.splice(Array.prototype.indexOf.call(node.childNodes, maps[move.item.key]), 1);
} else {
// 创建节点
insertNode = isObject(move.item)
? move.item.render()
: document.createTextNode(move.item);
}
// 同步staticNodeList信息
staticNodeList.splice(index, 0, insertNode);
// 操作Dom
node.insertBefore(insertNode, node.childNodes[index] || null);
}
});
}
export default patch;
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
当这步完成后可直接查看效果
// 4. 比较两棵虚拟DOM树的不同
const patches = diff(tree, newTree);
// 5. 在真正的DOM元素上应用变更
patch(root, patches);
2
3
4
5
结果如图: