- Virtual Dom 即根据最终状态在内存中绘制出一棵 Virtual Dom Tree,使用 Diff 算法与现存的 Dom Tree 对比并更新。
- Virtual Dom 并不能提升性能, 直接操作 Dom 理论上是最快的。
/**
sel [string]: 选择器, 比如 'div#id.class1.class2'
data [any]: 该节点属性(包括style、class等)
children (Array[]Vnode): 子节点(也由此函数创建)
text [string]: 节点内部的 text
ele [HTMLElement]: Dom 元素
**/
function vnode(sel, data, children, text, elm) (
// 是否包含key,在list中元素变动时有些许性能影响
let key = data === undefined ? undefined : data.key;
return {sel: sel, data: data, children: children,
text: text, elm: elm, key: key};
}
并不是每次创建虚拟节点都需要那么多参数,下面会根据传入的参数来调整 vnode
的函数的产出:
/**
sel: 元素选择器
b: 如果是数组或包含sel属性的object, 则为子节点; 如果是string, 则是文本节点; 否则就是data
c: 如果存在, 就肯定是子节点,同时b是data(类型判断早于b)
**/
function h(sel, b, c) {
var data = {}, children, text, i;
if (c !== undefined) {
// ...
} else if (b !== undefined) {
// ...
}
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
// 如果该元素是字符串或数字, 那么就是个纯文本节点
if (is.primitive(children[i]))
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
return vnode(sel, data, children, text, undefined);
}
通过以上方法,就可以创建出一整棵 Dom Tree 了, 如下:
var data = [
{id: 65, content: 'A'},
{id: 66, content: 'B'},
{id: 67, content: 'C'},
];
var tree = h('ul', data.map(node => {
return h('li', {key: node.id}, node.content)
}))
// 得到的虚拟dom结构如下:
//
// A
// B
// C
//
这是从 Virtual Dom 映射到 Real Dom 的 main 函数:
// oldVNode: 可以是 HTMLElement(第一次调用) 也可以是上一次生成的虚拟Dom tree
// Vnode: 根据最新状态形成的虚拟Dom Tree
function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
// 用于生命周期 inserted 阶段,记录下所有新插入的节点以备调用
const insertedVnodeQueue: VNodeQueue = [];
// 整个 Diff 过程模块可注册的钩子(跳过)
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
if (!isVnode(oldVnode)) {
// 将 HTMLElement 转换成 VNode
oldVnode = emptyNodeAt(oldVnode);
}
// 如果两个节点相似(节点的 sel 与 key 完全相等)则更新
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else { // 否则直接替换
elm = oldVnode.elm as Node;
parent = api.parentNode(elm);
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 取代 oldNode 的位置
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 整个 Diff 过程所有节点可注册的节点插入后调用的钩子(跳过)
for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
}
// 整个 Diff 过程模块可注册的钩子(跳过)
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
可见,整个流程比较清晰,替换节点或者在节点相似时更新节点(注释中提到,代码通过比较key以及选择器来判断是否相似,当没有指定key时,那么元素只能通过选择器来判断)。下面处理节点更新细节的 patchVnode
函数:
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
let i: any, hook: any;
// 更新前调用的节点生命周期钩子
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
i(oldVnode, vnode);
}
const elm = vnode.elm = (oldVnode.elm as Node);
let oldCh = oldVnode.children;
let ch = vnode.children;
// 完全是同一个对象,不作处理
if (oldVnode === vnode) return;
// 更新时调用的生命周期钩子,包括模块以及节点自身的
if (vnode.data !== undefined) {
// 这里如果引入了class模块,那么就会更新class属性;引入style模块,则会更新元素的样式
// 此节点的更新都再这个循环里处理了
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
i = vnode.data.hook;
if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
}
// 开始处理子节点
// 如果不是纯文本
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh as Array, ch as Array, insertedVnodeQueue);
} else if (isDef(ch)) {
// 如果旧元素没有子节点,而新的有,那么简单的添加节点在此节点上
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
addVnodes(elm, null, ch as Array, 0, (ch as Array).length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 否则就是移除不该存在的子节点
removeVnodes(elm, oldCh as Array, 0, (oldCh as Array).length - 1);
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
api.setTextContent(elm, vnode.text as string);
}
// 生命周期钩子,更新完成后调用
if (isDef(hook) && isDef(i = hook.postpatch)) {
i(oldVnode, vnode);
}
}
整个框架的模块也是重要的组成部分。使用者可以加载不同的模块让框架选择性的更新 Dom 中元素的属性。
下面是某节点的所有子节点更新函数:
function updateChildren(parentElm: Node,
oldCh: Array,
newCh: Array,
insertedVnodeQueue: VNodeQueue) {
// 几个变量:
// 1. 旧子节点数组的 startIndex, endIndex, startNode, endNode
// 2. 新子节点数组的 startIndex, endIndex, startNode, endNode
let oldStartIdx = 0, newStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newEndIdx = newCh.length - 1;
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
let oldKeyToIdx: any;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 1,2,3,4,5 --> 2,3,4,5,1 (处理一些特殊情况?reverse也可以用到)
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node));
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
// 1,2,3,4,5 --> 5,1,2,3,4 (处理一些特殊情况?reverse也可以用到)
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 如果每个子元素都有 key(可识别),那麽乱序后大概率会进这里(list)
if (oldKeyToIdx === undefined) {
// 创建一个关于旧子元素数组的, key --> index 的映射(map)
// 没有key则不存在于map中
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 新子元素,获取该新元素在旧数组中的位置(如果有key)
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) { // New element
// 不存在在旧子元素数组中(如果没有指定key也会进入这里)
// 根据这个新元素创建dom元素插入
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
newStartVnode = newCh[++newStartIdx];
} else {
// 说明不是新元素,只是位置变了
elmToMove = oldCh[idxInOld]; // 与新元素对应的旧元素
if (elmToMove.sel !== newStartVnode.sel) {
// 虽然key相同,但是元素tag已经不同了
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
} else {
// 没大变,可能只是需要更新一下元素
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
// 这个节点处理过了,以后再循环到这里也不需要在处理了,置空
oldCh[idxInOld] = undefined as any;
// 更新完后简单的移动元素
api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
// 多余的删掉,缺的补上。。
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
整个子节点 Diff 的过程也比较简单。这样也可以解答使用过程中的一个疑惑了,“为什么给list中的元素添加唯一的key可以提升性能”?
因为在比对列表元素的过程中,一旦有了key值就可以复用之前的元素本身,而免去更新多数元素的过程了。比如原本是 [A, B, C, D, E]
, 此时往B与C之间插入元素F。
如果没有携带 key 值,整个过程就是将C更新为F,D更新为C,E更新为D,最后添加元素E;
如果携带了key值,则会进入最后一个 ifelse
,直接在B之后插入元素F,也不用更新其他任何元素。