# snabbdom 笔记
# 虚拟 dom 库
# diff 算法
对比两个列表的元素,通过算法来进行最小变化的更改,提升 dom 渲染的性能
# 核心的概念
# 1.虚拟 dom vnode
// 通过一个对象来表示dom的内容
{
tag: 'div',
props: {
id: 'app'
},
chidren: [
{
tag: 'p',
text: 'hello world!!!'
}
]
}
等同于
<div id="app"><p>hello world</p></div>
相关函数:
- vnode: 定义一个 vnode 结构,并返回
export interface VNode {
// 元素名称,包含id和类选择器
sel: string | undefined;
// vnode数据
data: VNodeData | undefined;
// 子vnode
children: Array<VNode | string> | undefined;
// 真实dom
elm: Node | undefined;
// 文本内容
text: string | undefined;
// 标识
key: Key | undefined;
}
// 伪代码
export function vnode (
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined
) => return { sel, data, children, text, elm, key };
- h: 调用 vnode 函数,创建一个 vnode
// 伪代码
export function h() {
// 序列化一些参数,sel,data,children
// 单独处理svg
addNS(data, children, sel);
// 返回
return vnode(sel, data, children, text, undefined);
}
# 2. patch 更新
path 函数会传两个参数,oldVnode 和 newVnode,最终目标是要将真实的 old Dom 变成 new Dom.oldVnode 可以为真实 dom 或者 vnode,el 元素必定包含真实 dom,否则就没有更新的意义
相关函数:
- init: 初始化 patch 函数
// 通过module可以自定义模块的插拔,domapi可以适配不同的平台
export function init(modules, domApi) {
// 传入vnode或者dom,来更新为新的vnode
return function patch(oldVnode, vnode) {
// 执行模块的钩子
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 将vnode转为dom
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
// 如果是同一个vnode,进行内部patch
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
// 否则直接插入新元素,删除旧元素
insertBefore;
removeVnodes;
}
// insertedVnodeQueue
// 执行钩子
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
}
- patchVnode: 对比两个 vnode 进行更新
function patchVnode(oldVnode, vnode, insertedVnodeQueue) {
// 1.如果oldVnode === vnode,直接return
// 2.如果传了data(意味着需要更新一些属性),执行各个模块的update函数,包含data.hooks内部的
// 3.如果新vnode没有定义text,则判断children
// 3.1.新旧vnode都有children , 并且不一样,则updateChildren(elm, oldCh, ch, insertedVnodeQueue);
// 3.2.只有新vnode有children,如果旧vnode有text,先清空,同时添加新vnode的节点
// 3.3.只有旧vnode有children,直接删除旧vnode
// 3.4.只有旧vnode有text,则删除text
// 4.如果新vnode定义了text,并且和原dom不一致
// 4.1.如果旧vnode有children,则直接删除旧vnode,同时更新内容为新vnode的text
}
- updateChildren: 核心的 diff 算法更新 dom
function(parentElm,oldCh,newCh,insertedVnodeQueue){
// 定义4个索引和4个vnode
let oldStartIdx = 0;
let oldEndIdx = oldCh.length - 1;
let newStartIdx = 0;
let newEndIdx = newCh.length - 1;
let oldStartVnode = oldCh[0];
let oldEndVnode = oldCh[oldEndIdx];
let newStartVnode = newCh[0];
let newEndVnode = newCh[newEndIdx];
// 两个vnode列表进行对比,通过指针的变化来移动
// 两个start index都超出end index的时候,循环终止
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 一开始会对比这4个vnode,old和new两两进行对比,最终将旧列表更新为新的列表
// 当某个vnode被直接选中的时候,会有一个null的空位,这个时候直接变化指针即可
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和新列表的最后一个vnode是同个vnode,就需要把旧的第一个vnode放到最后一个vnode的前面,使得和新的vnode列表一致
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 和上面正好相反,将oldEndVnode放到第一个oldStartVnode的后面一个即可
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 如果没有找个对应的相同元素,每次都拿新vnode列表的第一个vnode去旧vnode列表去找
if (oldKeyToIdx === undefined) {
// 做一个map,{key:index}
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 新vnode列表的newStartVnode作为key,去旧vnode列表寻找
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 如果没有找到,则是一个新元素,直接插入
if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
// 如果存在index
elmToMove = oldCh[idxInOld];
// key相同,属性有变化,直接插入新的vnode
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!
);
} else {
// 不然就更新两个vnode,将找到的element放到第一个oldStartVnode的后面一个即可
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
// 更新指针
newStartVnode = newCh[++newStartIdx];
}
}
}
# 3. modules
通过模块的形式注入相关的更新
- style
- class
- prop
- event ...
每个模块都有对应的钩子去执行逻辑
// 比如class
export const classModule: Module = { create: updateClass, update: updateClass };