snabbdom是比较著名的虚拟dom库,Vue2 diff算法就是借鉴了snabbdom的构思。本文主要对源码核心部分解读,实现一个简易版的snabbdom
jsimport {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
// 创建path函数
const patch = init([
classModule,
propsModule,
styleModule, // 样式模块
eventListenersModule, // 事件监听模块
]);
const container = document.getElementById("container");
const someFn = () => {};
const anotherEventHandler = () => { };
// 创建虚拟节点
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
// 虚拟节点上树
patch(container, vnode);
// 新的虚拟节点
const newVnode = h(
"div#container.two.classes",
{ on: { click: anotherEventHandler } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
// 新旧虚拟节点比较
patch(vnode, newVnode);
h函数核的核心部分如下
tsimport { vnode, VNode, VNodeData } from "./vnode";
import * as is from "./is";
export type VNodes = VNode[];
export type VNodeChildElement =
| VNode
| string
| number
| String
| Number
| undefined
| null;
export type ArrayOrElement<T> = T | T[];
export type VNodeChildren = ArrayOrElement<VNodeChildElement>;
/** 类似于函数重载 */
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData | null): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(
sel: string,
data: VNodeData | null,
children: VNodeChildren
): VNode;
export function h(sel: any, b?: any, c?: any) :VNode {
let data: VNodeData = {};
let children: any;
let text: any;
let i: number;
if (c !== undefined) {
// 第三个参数不为空,h('div',{},'文本') 或 h('div',{},[]) 或 h('div',{},h())
if (b !== null) {
//第二个参数不为空,数据对象data
data = b;
}
if (is.array(c)) {
children = c;
} else if (is.primitive(c)) {
// 字符串或数字类型
text = c.toString();
} else if (c && c.sel) {
//第三个参数是vnode
children = [c];
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
// h('div',[])
children = b;
} else if (is.primitive(b)) {
// h('div','文本')
text = b.toString();
} else if (b && b.sel) {
// h('div',vnode)
children = [b];
} else {
data = b; // h('div',{})
}
}
if (children !== undefined) {
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);
}
vnode源码解读如下
tsimport { Hooks } from "./hooks";
import { Props } from "./modules/props";
import { Attrs } from "./modules/attributes";
import { Classes } from "./modules/class";
import { VNodeStyle } from "./modules/style";
import { Dataset } from "./modules/dataset";
import { On } from "./modules/eventlisteners";
import { AttachData } from "./helpers/attachto";
export type Key = string | number | symbol;
export interface VNode {
sel: string | undefined;
data: VNodeData | undefined;
children: Array<VNode | string> | undefined;
elm: Node | undefined;
text: string | undefined;
key: Key | undefined;
}
export interface VNodeData {
props?: Props;
attrs?: Attrs;
class?: Classes;
style?: VNodeStyle;
dataset?: Dataset;
on?: On;
attachData?: AttachData;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: any[]; // for thunks
is?: string; // for custom elements v1
[key: string]: any; // for any other 3rd party module
}
/**
*
* @param sel 字符串类型的 标签或选择器
* @param data 一个数据对象,以便在创建时访问或操作 DOM 元素、添加样式、操作 CSS classes、attributes 等
* @param children 子节点数组
* @param text 文本子节点
* @param elm 指向由 vnode 创建的真实 DOM 节点
* @returns
*/
export function vnode(
sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | DocumentFragment | Text | undefined
): VNode {
const key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
h.js如下
jsimport vnode from './vnode'
/**
* 简易版h函数,接受3个参数,如下3中调用方式
* 形态1 h('div',{},'文本')
* 形态2 h('div',{},[])
* 形态3 h('div',{},h())
*/
export default function (sel, data, c) {
if (arguments.length !== 3) {
throw new Error('这是低配版h函数,仅支持3个参数')
}
if (typeof c == 'string' || typeof c == 'number') {
// 形态1 调用方式
return vnode(sel, data, undefined, c, undefined)
} else if (Array.isArray(c)) {
// 形态2 h('div',{},[])
let children = []
for (let i = 0; i < c.length; i++) {
// 数组的每一项必须是一个h函数返回值
if (!(typeof c[i] == 'object' && c[i].hasOwnProperty('sel'))) {
throw new Error('第3个参数不是H函数')
}
children.push(c[i])
}
return vnode(sel,data,children,undefined,undefined)
} else if (typeof c == 'object' && c.hasOwnProperty('sel')) {
// h函数返回值是一个对象
// 形态3 h('div',{},h())
let children = [c]
return vnode(sel, data, children, undefined, undefined)
} else {
throw new Error('传入到参数有误')
}
}
vnode.js
jsexport default function (
sel,
data,
children,
text,
elm
) {
const key = data === undefined ? undefined : data.key;
return { sel, data, children, text, elm, key };
}
只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。
那么如何判定是否是同一虚拟节点呢? 答:选择器相同且key相同
只进行同层比较,不会进行跨层比较。即使是同一片 虚拟节点,但是跨层了,diff就是暴力删除旧的,然后插入新的
tsexport function init(
modules: Array<Partial<Module>>,
domApi?: DOMAPI,
options?: Options
) {
/**
* ...其它的辅助函数这里就不一一列举了
*/
/**
* 设计 4种命中查找策略
* 前:没有处理的节点中的第一个节点
* 后:没有处理的节点中的最后一个节点
* 新:newVnode
* 旧:oldVnode
* 1. 新前与旧前
* 2. 新后与旧后
* 3. 新后与旧前
* 4. 新前与旧后
* 这4种命中查找 从前往后顺序判断(命中一种就会命中判断了 ),如果4也没有命中就会在用循环的方式在旧节点中寻找,
* @param parentElm
* @param oldCh
* @param newCh
* @param insertedVnodeQueue
*/
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
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: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 跳过 置为undefined做标记(处理完毕了)
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)) { // 3. 新后与旧前,若命中将命中节点(新后指向的节点)移动到老节点旧后之后
// Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
// 将oldStartVnode.elm插入到oldEndVnode.elm下一个兄弟节点之前
api.insertBefore(
parentElm,
oldStartVnode.elm!,
api.nextSibling(oldEndVnode.elm!)
);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldEndVnode, newStartVnode)) { // 4. 新前与旧后,若命中,将命中的老节点(新前指向的节点)插入到老节点旧前之前
// Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);// 将命中的老节点插入到老节点中未处理的节点之前的位置
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
if (oldKeyToIdx === undefined) {
// 将Vnode的key作为map的key,下表索引作为value存入一个对象
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 在旧节点中查找 看是否存在newStartIdx对应的vnode,如果没找到,就是全新的节点需要新增。如果找到了,就是需要移动的节点
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 { // 同一节点 ,diff比较
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any; // 做标记,处理完该节点了
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
// 循环结束后,新节点childre中还有剩余的,代表剩余的就是要新增的
if (newStartIdx <= newEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
}
if (oldStartIdx <= oldEndIdx) { // 循环结束后,旧节点的children中还有剩余的,代表剩下的就是不要删除的
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
function patchVnode(
oldVnode: VNode,
vnode: VNode,
insertedVnodeQueue: VNodeQueue
) {
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
const elm = (vnode.elm = oldVnode.elm)!;
if (oldVnode === vnode) return;
if (
vnode.data !== undefined ||
(isDef(vnode.text) && vnode.text !== oldVnode.text)
) {
vnode.data ??= {};
oldVnode.data ??= {};
for (let i = 0; i < cbs.update.length; ++i)
cbs.update[i](oldVnode, vnode);
vnode.data?.hook?.update?.(oldVnode, vnode);
}
const oldCh = oldVnode.children as VNode[];
const ch = vnode.children as VNode[];
if (isUndef(vnode.text)) {//新节点没有text,意味着新节点有children
if (isDef(oldCh) && isDef(ch)) { // 新旧节点都有children,更新子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
} else if (isDef(ch)) { // 新节点有children,旧节点没有children ===》 清空旧节点的text,追加children子节点
if (isDef(oldVnode.text)) api.setTextContent(elm, "");
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
} else if (isDef(oldCh)) {// 老节点有children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
} else if (isDef(oldVnode.text)) { // 老节点有text,清空老节点的text
api.setTextContent(elm, "");
}
} else if (oldVnode.text !== vnode.text) { //新节点有text,并且老节点text !== 新节点text
if (isDef(oldCh)) { // 删除旧节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 将dom的text改为新节点的text
api.setTextContent(elm, vnode.text!);
}
hook?.postpatch?.(oldVnode, vnode);
}
/** path函数对老节点和新节点进行diff比较 */
return function patch(
oldVnode: VNode | Element | DocumentFragment,
vnode: VNode
): VNode {
let i: number, elm: Node, parent: Node;
const insertedVnodeQueue: VNodeQueue = [];
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
if (isElement(api, oldVnode)) {
// 老节点是DOMElement,不是vnode ====> dom元素第一次上树的时候 path(document.getElementById("container"),h())
oldVnode = emptyNodeAt(oldVnode); // 将初始化容器转换为vnode
} else if (isDocumentFragment(api, oldVnode)) {
oldVnode = emptyDocumentFragmentAt(oldVnode);
}
if (sameVnode(oldVnode, vnode)) {//是同一节点进行精细化比较
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
//不是同一节点,创建新节点,删除旧节点
elm = oldVnode.elm!;
parent = api.parentNode(elm) as Node;//获取旧节点的父节点
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
removeVnodes(parent, [oldVnode], 0, 0);
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
};
}
判断dom节点类型
ts/** 是否是dom元素 */
function isElement(node: Node): node is Element {
return node.nodeType === 1;
}
/** 元素的文本节点 */
function isText(node: Node): node is Text {
return node.nodeType === 3;
}
/** 是否是注释 */
function isComment(node: Node): node is Comment {
return node.nodeType === 8;
}
/** 是否是文档片段 */
function isDocumentFragment(node: Node): node is DocumentFragment {
return node.nodeType === 11;
}

patch.js
jsimport vnode from "./vnode";
import createElement from "./createElement";
export default function (oldVnode, newVnode) {
// 判断传入的第一个参数是 DOM节点 还是 虚拟节点
if (oldVnode.sel == "" || oldVnode.sel === undefined) {
// 说明oldVnode是DOM节点,此时要包装成虚拟节点
oldVnode = vnode(
oldVnode.tagName.toLowerCase(), // sel
{}, // data
[], // children
undefined, // text
oldVnode // elm
);
}
// 判断 oldVnode 和 newVnode 是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
console.log("是同一个节点,需要精细化比较");
} else {
console.log("不是同一个节点,暴力 插入新节点,删除旧节点");
// 创建 新虚拟节点 为 DOM节点
// 要操作DOM,所以都要转换成 DOM节点
let newVnodeElm = createElement(newVnode);
let oldVnodeElm = oldVnode.elm;
// 插入 新节点 到 旧节点 之前
if (newVnodeElm) {
// 判断newVnodeElm是存在的 在旧节点之前插入新节点
oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm);
}
// 删除旧节点
oldVnodeElm.parentNode.removeChild(oldVnodeElm);
}
}
createElement.js 创建真实dom节点
js/**
* 创建节点。将vnode虚拟节点创建为DOM节点
* 是孤儿节点,不进行插入操作
* @param {object} vnode
*/
export default function createElement(vnode) {
// 根据虚拟节点sel选择器属性 创建一个DOM节点,这个节点现在是孤儿节点
let domNode = document.createElement(vnode.sel);
// 判断是有子节点还是有文本
if (
vnode.text !== "" &&
(vnode.children === undefined || vnode.children.length === 0)
) {
// 说明没有子节点,内部是文本
domNode.innerText = vnode.text;
} else if (Array.isArray(vnode.children) && vnode.children.length > 0) {
// 说明内部是子节点,需要递归创建节点
// 遍历数组
for (let ch of vnode.children) {
// 递归调用 创建出它的DOM,一旦调用createElement意味着创建出DOM了。并且它的elm属性指向了创建出的dom,但是没有上树,是一个孤儿节点
let chDOM = createElement(ch); // 得到 子节点 表示的 DOM节点 递归最后返回的一定是文本节点
// 文本节点 上domNode树
domNode.appendChild(chDOM);
}
}
// 补充虚拟节点的elm属性
vnode.elm = domNode;
// 返回domNode DOM对象
return domNode;
}

源码部分如下

js// 判断 oldVnode 和 newVnode 是不是同一个节点
if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) {
console.log("是同一个节点,需要精细化比较");
patchVnode(oldVnode, newVnode);
}
patchVnode.js
jsexport default function patchVnode(oldVnode, newVnode) {
// 1. 判断新旧 vnode 是否是同一个对象
if (oldVnode === newVnode) return;
// 2. 判断 newVndoe 有没有 text 属性
if (
newVnode.text !== undefined &&
(newVnode.children === undefined || newVnode.children.length === 0)
) {
// newVnode 有 text 属性
// 2.1 判断 newVnode 与 oldVnode 的 text 属性是否相同
if (newVnode.text !== oldVnode.text) {
// 如果newVnode中的text和oldVnode的text不同,那么直接让新text写入老elm中即可。
// 如果oldVnode中是children,也会立即消失
oldVnode.elm.innerText = newVnode.text;
}
} else {
// newVnode 没有text属性 有children属性
// 2.2 判断 oldVnode 有没有 children 属性
if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
// oldVnode有children属性 最复杂的情况,新老节点都有children
} else {
// oldVnode没有children属性 说明有text; newVnode有children属性
// 清空oldVnode的内容
oldVnode.elm.innerHTML = "";
// 遍历新的vnode虚拟节点的子节点,创建DOM,上树
for (let ch of newVnode.children) {
let chDOM = createElement(ch);
oldVnode.elm.appendChild(chDOM);
}
}
}
}
经典的diff算法优化策略

该算算也被叫做双端比较

ts /**
* 设计 4种命中查找策略
* 前:没有处理的节点中的第一个节点
* 后:没有处理的节点中的最后一个节点
* 新:newVnode
* 旧:oldVnode
* 1. 新前与旧前
* 2. 新后与旧后
* 3. 新后与旧前
* 4. 新前与旧后
* 这4种命中查找 从前往后顺序判断(命中一种就会命中判断了 ),如果4也没有命中就会在用循环的方式在旧节点中寻找,
* @param parentElm
* @param oldCh
* @param newCh
* @param insertedVnodeQueue
*/
function updateChildren(
parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue
) {
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: KeyToIndexMap | undefined;
let idxInOld: number;
let elmToMove: VNode;
let before: any;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 跳过 置为undefined做标记(处理完毕了)
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)) { // 3. 新后与旧前,若命中将命中节点(新后指向的节点)移动到老节点旧后之后
// 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)) { // 4. 新前与旧后,若命中,将命中的老节点(新前指向的节点)插入到老节点旧前之前
// 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);
}
// 在旧节点中查找 看是否存在newStartIdx对应的vnode,如果没找到,就是全新的节点需要新增。如果找到了,就是需要移动的节点
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 { // 同一节点 ,diff比较
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
oldCh[idxInOld] = undefined as any; // 做标记,处理完该节点了
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
}
}
newStartVnode = newCh[++newStartIdx];
}
}
// 循环结束后,新节点childre中还有剩余的,代表剩余的就是要新增的
if (newStartIdx <= newEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
addVnodes(
parentElm,
before,
newCh,
newStartIdx,
newEndIdx,
insertedVnodeQueue
);
}
if (oldStartIdx <= oldEndIdx) { // 循环结束后,旧节点的children中还有剩余的,代表剩下的就是不要删除的
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
如果命中①了,patch之后就移动头指针 newStart++ oldStart++
如果没命中就接着比较下一种情况

如果命中②了,patch后就移动尾指针 newEnd-- oldEnd–
如果没命中就接着比较下一种情况

如果命中③了,将 新后newEnd 指向的节点移动到 旧后oldEnd 之后
如果没命中就接着比较下一种情况

如果命中④了,将 新前newStart 指向的节点,移动到 旧前oldStart 之前
如果没命中就表示四种情况都没有命中

找到了就 移动位置 移动指针newStart++

没找的就是新节点,直接插入所有未处理旧节点之前
循环结束后
newVnode中还有剩余
新节点中剩余的都 插入 旧节点oldEnd后面 或 oldStart之前





patchVnode.js
js// 2.2 判断 oldVnode 有没有 children 属性
if (oldVnode.children !== undefined && oldVnode.children.length > 0) {
// oldVnode有children属性 最复杂的情况,新老节点都有children
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children);
}
updateChildren.js
jsimport createElement from "./createElement";
import patchVnode from "./patchVnode";
/**
*
* @param {object} parentElm Dom节点
* @param {Array} oldCh oldVnode的子节点数组
* @param {Array} newCh newVnode的子节点数组
*/
export default function updateChildren(parentElm, oldCh, newCh) {
console.log("updateChildren()");
console.log(oldCh, newCh);
// 四个指针
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 指针指向的四个节点
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
let keyMap = null;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
console.log("**循环中**");
// 首先应该不是判断四种命中,而是略过已经加了undefined标记的项
if (oldStartVnode === null || oldCh[oldStartIdx] === undefined) {
oldStartVnode = oldCh[++oldStartIdx];
} else if (oldEndVnode === null || oldCh[oldEndIdx] === undefined) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newStartVnode === null || newCh[newStartIdx] === undefined) {
newStartVnode = newCh[++newStartIdx];
} else if (newEndVnode === null || newCh[newEndIdx] === undefined) {
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newStartVnode)) {
// 新前与旧前
console.log(" ①1 新前与旧前 命中");
// 精细化比较两个节点 oldStartVnode现在和newStartVnode一样了
patchVnode(oldStartVnode, newStartVnode);
// 移动指针,改变指针指向的节点,这表示这两个节点都处理(比较)完了
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (checkSameVnode(oldEndVnode, newEndVnode)) {
// 新后与旧后
console.log(" ②2 新后与旧后 命中");
patchVnode(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newEndVnode)) {
// 新后与旧前
console.log(" ③3 新后与旧前 命中");
patchVnode(oldStartVnode, newEndVnode);
// 当③新后与旧前命中的时候,此时要移动节点。移动 新后(旧前) 指向的这个节点到老节点的 旧后的后面
// 移动节点:只要插入一个已经在DOM树上 的节点,就会被移动
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldEndVnode, newStartVnode)) {
// 新前与旧后
console.log(" ④4 新前与旧后 命中");
patchVnode(oldEndVnode, newStartVnode);
// 当④新前与旧后命中的时候,此时要移动节点。移动 新前(旧后) 指向的这个节点到老节点的 旧前的前面
// 移动节点:只要插入一个已经在DOM树上的节点,就会被移动
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 四种都没有匹配到,都没有命中
console.log("四种都没有命中");
// 寻找 keyMap 一个映射对象, 就不用每次都遍历old对象了
if (!keyMap) {
keyMap = {};
// 记录oldVnode中的节点出现的key
// 从oldStartIdx开始到oldEndIdx结束,创建keyMap
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if (key !== undefined) {
keyMap[key] = i;
}
}
}
console.log(keyMap);
// 寻找当前项(newStartIdx)在keyMap中映射的序号
const idxInOld = keyMap[newStartVnode.key];
if (idxInOld === undefined) {
// 如果 idxInOld 是 undefined 说明是全新的项,要插入
// 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
} else {
// 说明不是全新的项,要移动
const elmToMove = oldCh[idxInOld];
patchVnode(elmToMove, newStartVnode);
// 把这项设置为undefined,表示我已经处理完这项了
oldCh[idxInOld] = undefined;
// 移动,调用insertBefore也可以实现移动。
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
}
// newStartIdx++;
newStartVnode = newCh[++newStartIdx];
}
}
// 循环结束
if (newStartIdx <= newEndIdx) {
// 说明newVndoe还有剩余节点没有处理,所以要添加这些节点
// // 插入的标杆
// const before =
// newCh[newEndIdx + 1] === null ? null : newCh[newEndIdx + 1].elm;
for (let i = newStartIdx; i <= newEndIdx; i++) {
// insertBefore方法可以自动识别null,如果是null就会自动排到队尾,和appendChild一致
parentElm.insertBefore(createElement(newCh[i]), oldCh[oldStartIdx].elm);
}
} else if (oldStartIdx <= oldEndIdx) {
// 说明oldVnode还有剩余节点没有处理,所以要删除这些节点
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm);
}
}
}
}
// 判断是否是同一个节点
function checkSameVnode(a, b) {
return a.sel === b.sel && a.key === b.key;
}


本文作者:千寻
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!