2023-06-25
JavaScript
00
请注意,本文编写于 902 天前,最后修改于 640 天前,其中某些信息可能已经过时。

目录

snabbdom的基本使用
源码解读 h函数
用js实现简易版的h函数
diff算法
原理须知
path函数源码解读
大致的流程如下
手写虚拟dom 首次上树
处理新旧节点是同一节点的情况
思路分析
js实现新旧节点text不同情况
新旧vnode都有children分析
源码解读
循环四种命中查找
比较新前newStart 与 旧前oldStart
比较 ② 新后newEnd 与 旧后 oldEnd
比较 ③ 新后newEnd 与 旧前oldStart
比较 ④ 新前newStart 与 旧后oldEnd
四种都没命中 遍历oldVnode中的key
循环结束
手写diff更新子节点操作

snabbdom是比较著名的虚拟dom库,Vue2 diff算法就是借鉴了snabbdom的构思。本文主要对源码核心部分解读,实现一个简易版的snabbdom

snabbdom的基本使用

js
import { 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函数

h函数核的核心部分如下

ts
import { 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源码解读如下

ts
import { 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 }; }

用js实现简易版的h函数

h.js如下

js
import 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

js
export default function ( sel, data, children, text, elm ) { const key = data === undefined ? undefined : data.key; return { sel, data, children, text, elm, key }; }

diff算法

原理须知

只有是同一个虚拟节点,才进行精细化比较,否则就是暴力删除旧的、插入新的。

那么如何判定是否是同一虚拟节点呢? 答:选择器相同且key相同

只进行同层比较,不会进行跨层比较。即使是同一片 虚拟节点,但是跨层了,diff就是暴力删除旧的,然后插入新的

path函数源码解读

ts
export 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; }

大致的流程如下

clipboard-2023-06-26.png

手写虚拟dom 首次上树

patch.js

js
import 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; }

处理新旧节点是同一节点的情况

思路分析

clipboard-2023-06-27.png

源码部分如下

clipboard-2023-06-27.png

js实现新旧节点text不同情况

js
// 判断 oldVnode 和 newVnode 是不是同一个节点 if (oldVnode.key === newVnode.key && oldVnode.sel === newVnode.sel) { console.log("是同一个节点,需要精细化比较"); patchVnode(oldVnode, newVnode); }

patchVnode.js

js
export 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); } } } }

新旧vnode都有children分析

经典的diff算法优化策略

image.png

该算算也被叫做双端比较

clipboard-2023-06-27.png

源码解读

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); } }

循环四种命中查找

比较新前newStart 与 旧前oldStart

如果命中①了,patch之后就移动头指针 newStart++ oldStart++

如果没命中就接着比较下一种情况

clipboard-2023-06-27.png

比较 ② 新后newEnd 与 旧后 oldEnd

如果命中②了,patch后就移动尾指针 newEnd-- oldEnd–

如果没命中就接着比较下一种情况

clipboard-2023-06-27.png

比较 ③ 新后newEnd 与 旧前oldStart

如果命中③了,将 新后newEnd 指向的节点移动到 旧后oldEnd 之后

如果没命中就接着比较下一种情况

clipboard-2023-06-27.png

比较 ④ 新前newStart 与 旧后oldEnd

如果命中④了,将 新前newStart 指向的节点,移动到 旧前oldStart 之前

如果没命中就表示四种情况都没有命中

clipboard-2023-06-27.png

四种都没命中 遍历oldVnode中的key

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

clipboard-2023-06-27.png

没找的就是新节点,直接插入所有未处理旧节点之前

循环结束

循环结束后

  • newVnode中还有剩余

    新节点中剩余的都 插入 旧节点oldEnd后面 或 oldStart之前

clipboard-2023-06-27.png

clipboard-2023-06-27.png

  • oldVnode中还有剩余节点

image.png

image.png

image.png

手写diff更新子节点操作

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

js
import 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; }
如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:千寻

本文链接:

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