文章内容输出来源:拉勾教育前端高薪训练营
Vitual DOM(虚拟DOM),是由普通的JS对象来描述的DOM对象
vue使用的底层虚拟DOM库
Snabbdom的核心
import { init, h } from 'snabbdom'
// 参数数组中传递Snabbdom的模块
const patch = init([])
// h函数第一个参数:标签+选择器,第二个参数:如果是字符串就是标签中的文本内容
const vnode = h('div#container.test', 'Hello World')
const app = document.querySelector('#app')
// patch函数对比两个VNode更新差异
// 第一个参数:旧的VNode或者DOM元素,第二个参数:新的VNode
const oldVNode = patch(app, vnode)
setTimeout(() => {
const newVNode = h('div#container.vnode', [
h('h1', 'Hello Snabbdom'),
h('p', 'xxxxxxxx')
])
patch(oldVNode, newVNode)
// 清空节点内容
// patch(oldVNode, h('!'))
}, 5000)
官方提供的模块
import {
init,
styleModule,
eventListenersModule,
h,
} from 'snabbdom'
let oldVNode
const handleClick = vnode => {
const newVNode = h('div#container.vnode2', [
h('h1', {
style: {
color: 'blue',
},
}, 'Hello Snabbdom'),
h('p', {
on: {
click: handleClick,
},
}, '请点击我')
])
oldVNode = patch(oldVNode, newVNode)
}
const patch = init([
styleModule,
eventListenersModule,
])
// h函数第一个参数:标签+选择器,第二个参数:如果是字符串就是标签中的文本内容
const vnode = h('div#container.test', 'Hello World')
const app = document.querySelector('#app')
// patch函数对比两个VNode更新差异
// 第一个参数:旧的VNode或者DOM元素,第二个参数:新的VNode
oldVNode = patch(app, vnode)
setTimeout(() => {
const newVNode = h('div#container.vnode2', [
h('h1', {
style: {
color: 'red',
},
}, 'Hello Snabbdom'),
h('p', {
on: {
click: handleClick,
},
}, '请点击我')
])
oldVNode = patch(oldVNode, newVNode)
}, 5000)
h函数的作用是创建VNode对象
函数重载
function add (a: number, b: number) {
console.log(a + b)
}
function add (a: number, b: number, c: number) {
console.log(a + b + c)
}
function add (a: number, b: string) {
console.log(a + b)
}
add(1, 2) // 执行第一个add函数
add(1, 2, 3) // 执行第二个add函数
add(1, '2') // 执行第三个add函数
// h函数的实现
export function h(sel, b, c) {
let data = {};
let children;
let text;
let i;
if (c !== undefined) { // 判读第三个参数是否传递
if (b !== null) { // 判断第二个参数属性对象是否不为null
data = b;
}
if (is.array(c)) { // 判读第三个参数是否为数组,为数组就存在子节点
children = c;
}
else if (is.primitive(c)) { // 判读第三个参数是否string或number文本类型
text = c;
}
else if (c && c.sel) { // 判读第三个参数是为单个节点,封装成数组形式
children = [c];
}
}
else if (b !== undefined && b !== null) { // 当第三个参数未传递,判断第二个参数是否传递,判断方法同上
if (is.array(b)) {
children = b;
}
else if (is.primitive(b)) {
text = b;
}
else if (b && b.sel) {
children = [b];
}
else {
data = b;
}
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) // 遍历children,判断children[i]是否为文本类型
children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
if (sel[0] === "s" && // 判断是否为svg
sel[1] === "v" &&
sel[2] === "g" &&
(sel.length === 3 || sel[3] === "." || sel[3] === "#")) {
addNS(data, children, sel);
}
return vnode(sel, data, children, text, undefined);
}
// vnode实现
// children和text互斥
export interface VNode {
sel: string | undefined;
data: VNodeData | undefined;
children: Array<VNode | string> | undefined;
elm: Node | undefined;
text: string | undefined;
key: Key | undefined;
}
export function vnode(sel, data, children, text, elm) {
const key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
patchVnode工作流程
Diff算法
执行过程:对开始和结束节点比较,总共四种情况
oldStartVnode / newStartVnode(旧开始节点 / 新开始节点)
oldEndVnode / newEndVnode(旧结束节点 / 新结束节点)
oldStartVnode / newEndVnode(旧开始节点 / 新结束节点)
oldEndVnode / newStartVnode(旧结束节点 / 新开始节点)
oldStartVnode / newStartVnode(旧开始节点 / 新开始节点)比较
oldEndVnode / newEndVnode(旧结束节点 / 新结束节点)比较
oldStartVnode / newEndVnode(旧开始节点 / 新结束节点)比较
oldEndVnode / newStartVnode(旧结束节点 / 新开始节点)比较
上述情况都不满足的情况下,在旧节点数组中依次查找是否有相同的新节点
循环结束收尾
// 判断是否为相同节点
function sameVnode(vnode1, vnode2) {
var _a, _b;
const isSameKey = vnode1.key === vnode2.key;
const isSameIs = ((_a = vnode1.data) === null || _a === void 0 ? void 0 : _a.is) === ((_b = vnode2.data) === null || _b === void 0 ? void 0 : _b.is);
const isSameSel = vnode1.sel === vnode2.sel;
return isSameSel && isSameKey && isSameIs;
}
// 判断是否是VNode对象
function isVnode(vnode) {
return vnode.sel !== undefined;
}
function createKeyToOldIdx(children, beginIdx, endIdx) {
var _a;
const map = {};
for (let i = beginIdx; i <= endIdx; ++i) {
const key = (_a = children[i]) === null || _a === void 0 ? void 0 : _a.key;
if (key !== undefined) {
map[key] = i;
}
}
return map;
}
// 定义钩子
const hooks = [
"create",
"update",
"remove",
"destroy",
"pre",
"post",
];
// modules:依赖模块,domApi:执行环境
export function init(modules, domApi) {
let i;
let j;
const cbs = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: [],
};
const api = domApi !== undefined ? domApi : htmlDomApi; // 设置默认执行环境为浏览器环境
for (i = 0; i < hooks.length; ++i) { // 遍历设置钩子函数 cbs => { create: [fn1, fn2...], update: [fn1, fn2...] }
cbs[hooks[i]] = [];
for (j = 0; j < modules.length; ++j) {
const hook = modules[j][hooks[i]];
if (hook !== undefined) {
cbs[hooks[i]].push(hook);
}
}
}
function emptyNodeAt(elm) { // 转换成VNode对象
const id = elm.id ? "#" + elm.id : "";
// elm.className doesn't return a string when elm is an SVG element inside a shadowRoot.
// https://stackoverflow.com/questions/29454340/detecting-classname-of-svganimatedstring
const classes = elm.getAttribute("class");
const c = classes ? "." + classes.split(" ").join(".") : "";
return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm);
}
function createRmCb(childElm, listeners) {
return function rmCb() {
if (--listeners === 0) { // 执行完所有的钩子函数才移除节点
const parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
// 把VNode节点转换成对应的DOM元素,并存储在VNode的elm属性中
function createElm(vnode, insertedVnodeQueue) {
var _a, _b;
let i;
let data = vnode.data;
if (data !== undefined) { // 执行用户设置的init钩子函数
const init = (_a = data.hook) === null || _a === void 0 ? void 0 : _a.init;
if (isDef(init)) {
init(vnode);
data = vnode.data;
}
}
// 将VNode转换成真实DOM
const children = vnode.children;
const sel = vnode.sel;
if (sel === "!") { // 创建注释节点
if (isUndef(vnode.text)) {
vnode.text = "";
}
vnode.elm = api.createComment(vnode.text);
}
else if (sel !== undefined) {
// Parse selector
const hashIdx = sel.indexOf("#");
const dotIdx = sel.indexOf(".", hashIdx);
const hash = hashIdx > 0 ? hashIdx : sel.length;
const dot = dotIdx > 0 ? dotIdx : sel.length;
const tag = hashIdx !== -1 || dotIdx !== -1
? sel.slice(0, Math.min(hash, dot))
: sel;
const elm = (vnode.elm =
isDef(data) && isDef((i = data.ns))
? api.createElementNS(i, tag, data) // 一般此处创建的是svg
: api.createElement(tag, data));
if (hash < dot)
elm.setAttribute("id", sel.slice(hash + 1, dot)); // 设置id
if (dotIdx > 0)
elm.setAttribute("class", sel.slice(dot + 1).replace(/\./g, " ")); // 设置类
for (i = 0; i < cbs.create.length; ++i)
cbs.create[i](emptyNode, vnode);
if (is.array(children)) { // 遍历children,创建createElm
for (i = 0; i < children.length; ++i) {
const ch = children[i];
if (ch != null) {
api.appendChild(elm, createElm(ch, insertedVnodeQueue));
}
}
}
else if (is.primitive(vnode.text)) { // 数值和字符串形式的text,创建文本节点
api.appendChild(elm, api.createTextNode(vnode.text));
}
const hook = vnode.data.hook;
if (isDef(hook)) {
(_b = hook.create) === null || _b === void 0 ? void 0 : _b.call(hook, emptyNode, vnode);
if (hook.insert) { // 将insert钩子函数存储到insertedVnodeQueue队列中
insertedVnodeQueue.push(vnode);
}
}
}
else {
vnode.elm = api.createTextNode(vnode.text); // sel为空,创建文本节点
}
return vnode.elm;
}
function addVnodes(parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx];
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
function invokeDestroyHook(vnode) {
var _a, _b;
const data = vnode.data;
if (data !== undefined) {
(_b = (_a = data === null || data === void 0 ? void 0 : data.hook) === null || _a === void 0 ? void 0 : _a.destroy) === null || _b === void 0 ? void 0 : _b.call(_a, vnode); // 判断用户是否传入destory钩子函数
for (let i = 0; i < cbs.destroy.length; ++i)
cbs.destroy[i](vnode); // 遍历执行cbs中的destory钩子
if (vnode.children !== undefined) {
for (let j = 0; j < vnode.children.length; ++j) { // 遍历子节点,递归调用invokeDestroyHook进行删除
const child = vnode.children[j];
if (child != null && typeof child !== "string") {
invokeDestroyHook(child);
}
}
}
}
}
function removeVnodes(parentElm, vnodes, startIdx, endIdx) {
var _a, _b;
for (; startIdx <= endIdx; ++startIdx) {
let listeners;
let rm;
const ch = vnodes[startIdx];
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch); // 触发destory钩子函数
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm, listeners);
for (let i = 0; i < cbs.remove.length; ++i)
cbs.remove[i](ch, rm);
const removeHook = (_b = (_a = ch === null || ch === void 0 ? void 0 : ch.data) === null || _a === void 0 ? void 0 : _a.hook) === null || _b === void 0 ? void 0 : _b.remove;
if (isDef(removeHook)) {
removeHook(ch, rm); // 用户传入来remove钩子函数时,需要手动调用rm函数
}
else {
rm();
}
}
else {
// Text node
api.removeChild(parentElm, ch.elm);
}
}
}
}
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
let oldStartIdx = 0;
let 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;
let idxInOld;
let elmToMove;
let before;
// 同级节点比较
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
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); // 创建老节点的key和Idx关系{ key1: 1, key2: 2... }
}
idxInOld = oldKeyToIdx[newStartVnode.key]; // 用新节点的key查找在老节点中对应key的Idx
if (isUndef(idxInOld)) {
// New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); // 新节点为新元素,插入到老节点开始节点之前
}
else {
elmToMove = oldCh[idxInOld]; // 将对应的key值的老节点赋值给elmToMove
if (elmToMove.sel !== newStartVnode.sel) { // 当老节点和新节点不是sameVnode
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); // 新节点为新元素,插入到老节点开始节点之前
}
else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); // 对比新旧节点更新差异
oldCh[idxInOld] = undefined;
api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); // 移动elmToMove到老节点开始节点之前
}
}
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, insertedVnodeQueue) {
var _a, _b, _c, _d, _e;
const hook = (_a = vnode.data) === null || _a === void 0 ? void 0 : _a.hook;
(_b = hook === null || hook === void 0 ? void 0 : hook.prepatch) === null || _b === void 0 ? void 0 : _b.call(hook, oldVnode, vnode); // 执行传入的prepatch钩子函数
const elm = (vnode.elm = oldVnode.elm);
const oldCh = oldVnode.children;
const ch = vnode.children;
if (oldVnode === vnode) // 新旧节点相同时直接返回
return;
if (vnode.data !== undefined) { // 遍历执行update钩子函数
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
(_d = (_c = vnode.data.hook) === null || _c === void 0 ? void 0 : _c.update) === null || _d === void 0 ? void 0 : _d.call(_c, oldVnode, vnode);
}
if (isUndef(vnode.text)) { // 新节点没有text属性
if (isDef(oldCh) && isDef(ch)) { // 新旧节点都有子节点
if (oldCh !== ch) // 新旧节点的子节点不相等
updateChildren(elm, oldCh, ch, insertedVnodeQueue); // 对比新旧节点的不同,并更新DOM
}
else if (isDef(ch)) { // 新节点有子节点
if (isDef(oldVnode.text)) // 老节点有text属性时,清空DOM元素的对应的textContent
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属性,清空DOM元素的对应的textContent
api.setTextContent(elm, "");
}
}
else if (oldVnode.text !== vnode.text) { // 新旧节点text属性不相等
if (isDef(oldCh)) { // 如果老节点有子节点,移除老节点的子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
api.setTextContent(elm, vnode.text); // 设置text到节点
}
(_e = hook === null || hook === void 0 ? void 0 : hook.postpatch) === null || _e === void 0 ? void 0 : _e.call(hook, oldVnode, vnode); // 执行传入的postpatch钩子函数
}
return function patch(oldVnode, vnode) {
let i, elm, parent;
const insertedVnodeQueue = []; // 新插入节点队列
for (i = 0; i < cbs.pre.length; ++i) // 触发pre钩子进行预处理
cbs.pre[i]();
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
if (sameVnode(oldVnode, vnode)) { // 通过key和sel属性判断是否相同节点
patchVnode(oldVnode, vnode, insertedVnodeQueue); // 对比节点的差异
}
else { // 不是相同节点
elm = oldVnode.elm; // 获取老节点元素
parent = api.parentNode(elm); // 获取父元素
createElm(vnode, insertedVnodeQueue); // 创建新VNode的DOM元素
if (parent !== null) {
api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); // 插入新节点
removeVnodes(parent, [oldVnode], 0, 0); // 移除老节点
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) { // 执行insert钩子函数
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs.post.length; ++i)
cbs.post[i]();
return vnode;
};
}
关于列表中的key值
以li为例