snabbdom (一)
大约 10 分钟
好久没有更东西了, vue 的虚拟 DOM 借鉴了开源库 snabbdom 的虚拟 DOM, 闲暇之余也开始啃了 snabbom snabbdom这是 github 仓库的地址
目录结构
├── h.ts 创建 vnode
├── hook.ts 需要用到的钩子函数
├── htmldomapi.ts 操作 DOM 的 api
├── init.ts 初始化 vnode, 添加钩子 patch 都在这里面
├── is.ts 两个工具方法 一个判断是否是数组, 一个判断是否是数字或者字符串
├── jsx.ts jsx
├── jsx-global.ts
├── thunk.ts
├── tovnode.ts 真实 DOM 转 vnode
├── vnode.ts vnode 类
├── tsconfig.json
├── helper
│ └── utattachto.tsils
└── modules 将 vnode 编译成真实 DOM 有些属性需要从里面的模块导入 每个模块都实现了 module.ts 中的几种方法
├── attributes.ts 属性模块
├── class.ts 类模块
├── dataset.ts
├── eventlisteners.ts 事件监听
├── hero.ts
├── module.ts
├── props.ts 属性
└── style.ts 样式
init.ts
init 方法主要返回的是一个 patch 函数(我省略掉了代码 应该不会说我吧)
export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
return function patch();
}
分析
- 参数是一个依赖 Module 这个类型的数 这个 Module 就是 modules/module 导出来的 它定义了模块可能会有的某些方法, 第二参数就是 domApi 这个参数是可选的, 你可以传一些你自己定义的操作 dom 的方法进去
- tips Partial 这个是 typescript 中的类型依赖 我们只是需要 Module 中的某些属性即可
接着往下看
let i: number;
let j: number;
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: [],
};
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]];
// hook = modules[i].create ...
if (hook !== undefined) {
// 如果导入的模块有这个方法
(cbs[hooks[i]] as any[]).push(hook); // 将传入的module中的方法添加进 ModuleHooks中
}
}
}
分析
- 首先定义的 i, j 是为了下面的两重 for 循环
- cbs 首先看 ModuleHooks 这个类型是 Module 里面存在的键值 cbs 里面也都必须存在
- 判断是否传入 api 没传就用本身的 传了就用传过来的
- 下面的第一重 for 给 cbs 赋值 大概变成这样 {create: [], update: []...}
- 第二重 for 循环 是给 cbs[create]这样赋值, 先判断 module 里面有没有 create, update 以及其他四个 有 就 push 进去 最后大概是这样 cbs: {create: [fn1, fn2], update: [fn5]...}
// 将真实dom转成一个空的虚拟DOM
function emptyNodeAt(elm: Element) {
const id = elm.id ? '#' + elm.id : ''; // 如果存在id选择器 则拼接成 '#' + id
const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''; // 如果存在类选择器 则将所有的类拆开 以.拼接
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); // 调用vnode方法 传入五个参数 返回的是一个VNode对象
}
function createRmCb(childElm: Node, listeners: number) {
// 返回删除节点的方法
return function rmCb() {
if (--listeners === 0) {
// 高阶函数生成闭包 只有在listeners 为0了 才一起调用
const parent = api.parentNode(childElm) as Node;
api.removeChild(parent, childElm);
}
};
}
// 根据虚拟DOM创建真实的DOM 并返回
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any;
let data = vnode.data; // data里面放的就是 属性/事件/样式
// 执行init钩子函数
if (data !== undefined) {
const init = data.hook?.init; // 这个init是用户自定义的init
if (isDef(init)) {
// 判断init是否定义
init(vnode); // 重新执行一遍 是为了和用户所想要的方向达成一致
data = vnode.data; // 重新赋值
}
}
// 将vnode转换成真实的Dom
const children = vnode.children; // 取出子节点
const sel = vnode.sel; // 取出选择器
if (sel === '!') {
// 说明是注释节点
if (isUndef(vnode.text)) {
// 没有定义text属性的时 说明就只是一个空注释 <!---->
vnode.text = ''; // 赋值为空
}
vnode.elm = api.createComment(vnode.text!); // 创建一个注释节点 <!--vnode.text-->
} else if (sel !== undefined) {
// 不为空
// Parse selector
const hashIdx = sel.indexOf('#'); // 找到id选择器开始位置
const dotIdx = sel.indexOf('.', hashIdx); // 从#位置往后 开始寻找 .
const hash = hashIdx > 0 ? hashIdx : sel.length; // 如果存在id 选择器
const dot = dotIdx > 0 ? dotIdx : sel.length; // 存在类选择器
const tag =
hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel; // 从0开始 找到类选择器.开始位置 和 id选择器位置最小的 开始截取 出标签名 : 否则就是sel就是标签名
const elm = (vnode.elm =
isDef(data) && isDef((i = data.ns)) // 判断data是否存在 或者是否存在命名空间的标签
? api.createElementNS(i, tag) // 调用生成命名空间的
: api.createElement(tag)); // 调用生成一般标签的
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)); // 设置id名
if (dotIdx > 0)
elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')); // 设置类名
// 执行模块中的create 钩子
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode);
// 如果 vnode有子节点 则创建子vnode对应的dom 添加到dom上
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue));
}
}
} else if (is.primitive(vnode.text)) {
//如果text 是number/string
api.appendChild(elm, api.createTextNode(vnode.text)); // 创建成文本节点 并插入dom中
}
const hook = vnode.data!.hook;
if (isDef(hook)) {
// 执行用户传入的create
hook.create?.(emptyNode, vnode); // 这里的?表示 如果传入了create方法 就将后面的参数传入执行 否则 就不执行
if (hook.insert) {
// 存在insert 便追加进队列 为了让后面执行insert钩子
insertedVnodeQueue.push(vnode);
}
}
} else {
// 否则 创建文本节点
vnode.elm = api.createTextNode(vnode.text!);
}
// 返回创建的dom
return vnode.elm;
}
// 插入vnode
function addVnodes(
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue,
) {
// 父节点, 需要插入到之前位置的节点, 插入的vnode数组, 开始位置, 结束位置 插入节点队列
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
// 触发销毁钩子
function invokeDestroyHook(vnode: VNode) {
const data = vnode.data;
if (data !== undefined) {
data?.hook?.destroy?.(vnode); // 调用用户的destroy
for (let i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); // 调用模块中的destroy
if (vnode.children !== undefined) {
// 如果存在子节点
for (let j = 0; j < vnode.children.length; ++j) {
const child = vnode.children[j];
if (child != null && typeof child !== 'string') {
// 子节点存在 且 子节点不是字符串
invokeDestroyHook(child); // 递归
}
}
}
}
}
// 移出vnode
function removeVnodes(
parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number,
): void {
// 1. 父节点 2. 需要删除的节点数组 3 开始删除索引 4. 结束删除位置索引
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number;
let rm: () => void;
const ch = vnodes[startIdx]; // 拿到传过vnode数组中的 vnode
if (ch != null) {
//如果不为空
if (isDef(ch.sel)) {
// 判断是否是vnode
invokeDestroyHook(ch); // 触发destroy钩子函数
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm!, listeners); // 创建删除的回调函数
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); // 调用模块中的remove钩子
const removeHook = ch?.data?.hook?.remove;
if (isDef(removeHook)) {
// 如果用户设置了remove的钩子
removeHook(ch, rm); // 调用用户设置的remove
} else {
// 没有设置 就直接调用删除元素的方法
rm();
}
} else {
// Text node
// 如果是文本节点 直接调用删除文本节点的方法
api.removeChild(parentElm, ch.elm!);
}
}
}
}
// snabbdom的diff算法
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue,
) {
let oldStartIdx = 0; // 旧Vnode[]的第一个位置的节点下标
let newStartIdx = 0; // 新vnode[]的第一个位置的节点下标
let oldEndIdx = oldCh.length - 1; // 旧vnode[]最后一个节点下标
let oldStartVnode = oldCh[0]; // 旧vnode[]的第一个节点
let oldEndVnode = oldCh[oldEndIdx]; // 旧vnode[]的最后一个节点
let newEndIdx = newCh.length - 1; // 新vnode[]的最后一个节点下标
let newStartVnode = newCh[0]; // 新vnode[]的第一个节点
let newEndVnode = newCh[newEndIdx]; // 新vnode[]的最后一个节点
let oldKeyToIdx: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) {
// 旧vnode[]第一个vnode如果为null 则右移
oldStartVnode = oldCh[++oldStartIdx];
} else if (oldEndVnode == null) {
// 旧vnode[]倒数第一个vnode 如果为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
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!),
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) {
// New element
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!,
);
} else {
elmToMove = oldCh[idxInOld];
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(
parentElm,
createElm(newStartVnode, insertedVnodeQueue),
oldStartVnode.elm!,
);
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any;
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
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);
}
}
}
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue,
) {
const hook = vnode.data?.hook; // 获取用户设置钩子对象
hook?.prepatch?.(oldVnode, vnode); // 如果设置了prepatch钩子 便执行
const elm = (vnode.elm = oldVnode.elm!); // 保存对应的真实DOM 并赋值给新vnode的elm
const oldCh = oldVnode.children as VNode[]; // 保存旧vnode的所有子节点
const ch = vnode.children as VNode[]; // 保存新vnode的所有子节点
if (oldVnode === vnode) return; // 如果新旧都相同 没有打补丁不要
if (vnode.data !== undefined) {
// 新vnode的data存在
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); // 调用模块中的 update方法
vnode.data.hook?.update?.(oldVnode, vnode); // 用户自定义了 就调用用户自定的update
}
// 如果新vnode的text属性未定义
if (isUndef(vnode.text)) {
// 先判断旧节点的自节点 和新节点的子节点 是否被定义
if (isDef(oldCh) && isDef(ch)) {
// 如果旧节点的子节点不等于新节点的子节点 就用diff算法 比较更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
} else if (isDef(ch)) {
// 如果新节点有子节点 而 旧节点没有子节点 同时 新节点没有text属性
//判断老节点有text 变清空它
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 将新节点中的子节点添加进来
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {
// 如果老节点有子节点 新节点没有子节点
// 便删除子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) {
// 老节点定义了text 新节点没有text
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 如果新旧的text属性不相等
// 更新文本
if (isDef(oldCh)) {
// 判断旧的是否有children
removeVnodes(elm, oldCh, 0, oldCh.length - 1); // 移出旧节点的所有子节点
}
api.setTextContent(elm, vnode.text!); //设置文本节点
}
hook?.postpatch?.(oldVnode, vnode);
}
分析 patchVnode 总的思路就是需要把 oldVnode 改造得和 newVnode 一样
- 比较两个 vnode 是不是相同的
- 如果新 vnode 是否有 data 属性
- 有则遍历调用 cbs 中的 update 方法, 如果新 vnode 的 data.hook.update 存在也要执行一次
- 如果新 vnode 是否有 text 属性
- newVnode 没有 text 属性
- 判断 oldVnode 和 newVnode 是否有 children 属性
- 两个都有 children => 则调用 updateChildren 进行 diff
- oldVnode 有 newVnode 没有
- 判断 oldVnode 是否有 text 属性
- 有则移出
- 判断 oldVnode 是否有 text 属性
- newVnode 有 oldVnode 没有 => 移出所有 children 节点
- oldVnode 存在 text 属性 => 移出 text
- 判断 oldVnode 和 newVnode 是否有 children 属性
- newVnode 的 text 与 olcVnode 的 text 不相同
- 判断 oldVnode 是否有 children 属性
- 存在则 移出所有的 children
- 设置 newVnode 的 text
- 判断 oldVnode 是否有 children 属性
- newVnode 没有 text 属性
- 调用 postpath 钩子
分析 updateChildren
- 先比较两端
- 旧 vnode 头 vs 新 vnode 头
- 旧 vnode 尾 vs 新 vnode 尾
- 旧 vnode 头 vs 新 vnode 尾
- 旧 vnode 尾 vs 新 vnode 头
- 首尾不一样的情况,寻找 key 相同的节点,找不到则新建元素
- 如果找到 key,但是,元素选择器变化了,也新建元素
- 如果找到 key,并且元素选择没变, 则移动元素
- 两个列表对比完之后,清理多余的元素,新增添加的元素