Virtual Dom能夠看作一棵模擬了DOM樹的JavaScript對象樹,其主要是經過vnode,實現一個無狀態的組件,當組件狀態發生更新時,而後觸發Virtual Dom數據的變化,而後經過Virtual Dom和真實DOM的比對,再對真實DOM更新。虛擬DOM其實就是一種模擬DOM的JavaScript數據結構。html
像SnabbDOM這種庫的虛擬DOM是以下的數據結構:vue
說到SnabbDOM可能你們不太知道,可是大名鼎鼎的VUE就是使用SnabbDOM來提供虛擬DOM。SnabbDOM中的VNode結構以下:node
export interface VNodeData { props?: Props; attrs?: Attrs; class?: Classes; style?: VNodeStyle; dataset?: Dataset; on?: On; hero?: Hero; attachData?: AttachData; hook?: Hooks; key?: Key; ns?: string; // for SVGs fn?: () => VNode; // for thunks args?: Array<any>; // for thunks [key: string]: any; // for any other 3rd party module } export function vnode(sel: string | undefined, data: any | undefined, children: Array<VNode | string> | undefined, text: string | undefined, elm: Element | Text | undefined): VNode { let key = data === undefined ? undefined : data.key; return {sel: sel, data: data, children: children, text: text, elm: elm, key: key}; } export default vnode;
可是這裏並無直接提供對外接口,而是提供了h方法來建立VNode:git
export function h(sel: string): VNode; export function h(sel: string, data: VNodeData): VNode; export function h(sel: string, text: string): VNode; export function h(sel: string, children: Array<VNode | undefined | null>): VNode; export function h(sel: string, data: VNodeData, text: string): VNode; export function h(sel: string, data: VNodeData, children: Array<VNode | undefined | null>): VNode; export function h(sel: any, b?: any, c?: any): VNode { var data: VNodeData = {}, children: any, text: any, i: number; if (c !== undefined) { data = b; if (is.array(c)) { children = c; } else if (is.primitive(c)) { text = c; } else if (c && c.sel) { children = [c]; } } else if (b !== undefined) { if (is.array(b)) { children = b; } else if (is.primitive(b)) { text = b; } else if (b && b.sel) { children = [b]; } else { data = b; } } if (is.array(children)) { for (i = 0; i < children.length; ++i) { if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined); } } if ( sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' && (sel.length === 3 || sel[3] === '.' || sel[3] === '#') ) { addNS(data, children, sel); } return vnode(sel, data, children, text, undefined); };
那麼在具體使用場景以下:github
const overviewView = (movies) => h('div.page', {style: fadeInOutStyle}, [ h('div.header', [ h('div.header-content.overview', { style: fadeInOutStyle, }, [ h('div.header-title', { style: {transform: 'translateY(-2em)', delayed: {transform: 'translate(0)'}, destroy: {transform: 'translateY(-2em)'}} }, 'Top 10 movies'), h('div.spacer'), ]), ]), h('div.page-content', [ h('div.list', { style: {opacity: '0', delayed: {opacity: '1'}, remove: {opacity: '0', position: 'absolute', top: '0', left: '0'}} }, movies.map((movie) => h('div.row', { on: {click: [select, movie]}, }, [ h('div.hero.rank', [ h('span.hero', {hero: {id: 'rank'+movie.rank}}, movie.rank) ]), h('div.hero', {hero: {id: movie.title}}, movie.title) ]) )), ]), ]);
其中跟虛擬DOM各個週期中須要使用的hook方法都放在Hook.ts這個文件下:算法
import {VNode} from './vnode'; export type PreHook = () => any; export type InitHook = (vNode: VNode) => any; export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any; export type InsertHook = (vNode: VNode) => any; export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any; export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any; export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any; export type DestroyHook = (vNode: VNode) => any; export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any; export type PostHook = () => any; export interface Hooks { pre?: PreHook; init?: InitHook; create?: CreateHook; insert?: InsertHook; prepatch?: PrePatchHook; update?: UpdateHook; postpatch?: PostPatchHook; destroy?: DestroyHook; remove?: RemoveHook; post?: PostHook; }
具體的鉤子方法則是放在src/modules目錄下的各個文件中。api
其中核心代碼,包括整個Diff算法都放在snabbdom.ts文件中:數據結構
/* global module, document, Node */ import {Module} from './modules/module'; import {Hooks} from './hooks'; import vnode, {VNode, VNodeData, Key} from './vnode'; import * as is from './is'; import htmlDomApi, {DOMAPI} from './htmldomapi'; function isUndef(s: any): boolean { return s === undefined; } function isDef(s: any): boolean { return s !== undefined; } type VNodeQueue = Array<VNode>; const emptyNode = vnode('', {}, [], undefined, undefined); function sameVnode(vnode1: VNode, vnode2: VNode): boolean { return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; } function isVnode(vnode: any): vnode is VNode { return vnode.sel !== undefined; } type KeyToIndexMap = {[key: string]: number}; type ArraysOf<T> = { [K in keyof T]: (T[K])[]; } type ModuleHooks = ArraysOf<Module>; function createKeyToOldIdx(children: Array<VNode>, beginIdx: number, endIdx: number): KeyToIndexMap { let i: number, map: KeyToIndexMap = {}, key: Key | undefined, ch; for (i = beginIdx; i <= endIdx; ++i) { ch = children[i]; if (ch != null) { key = ch.key; if (key !== undefined) map[key] = i; } } return map; } const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; export {h} from './h'; export {thunk} from './thunk'; export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) { let i: number, j: number, cbs = ({} as ModuleHooks); 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]]; if (hook !== undefined) { (cbs[hooks[i]] as Array<any>).push(hook); } } } function emptyNodeAt(elm: Element) { const id = elm.id ? '#' + elm.id : ''; const c = elm.className ? '.' + elm.className.split(' ').join('.') : ''; return vnode(api.tagName(elm).toLowerCase() + id + c, {}, [], undefined, elm); } function createRmCb(childElm: Node, listeners: number) { return function rmCb() { if (--listeners === 0) { const parent = api.parentNode(childElm); api.removeChild(parent, childElm); } }; } function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node { let i: any, data = vnode.data; if (data !== undefined) { if (isDef(i = data.hook) && isDef(i = i.init)) { i(vnode); data = vnode.data; } } let children = vnode.children, sel = vnode.sel; if (sel === '!') { if (isUndef(vnode.text)) { vnode.text = ''; } vnode.elm = api.createComment(vnode.text as string); } 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 as VNodeData).ns) ? api.createElementNS(i, tag) : api.createElement(tag); if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)); 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)) { 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)) { api.appendChild(elm, api.createTextNode(vnode.text)); } i = (vnode.data as VNodeData).hook; // Reuse variable if (isDef(i)) { if (i.create) i.create(emptyNode, vnode); if (i.insert) insertedVnodeQueue.push(vnode); } } else { vnode.elm = api.createTextNode(vnode.text as string); } return vnode.elm; } function addVnodes(parentElm: Node, before: Node | null, vnodes: Array<VNode>, startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue) { for (; startIdx <= endIdx; ++startIdx) { const ch = vnodes[startIdx]; if (ch != null) { api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before); } } } function invokeDestroyHook(vnode: VNode) { let i: any, j: number, data = vnode.data; if (data !== undefined) { if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode); for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode); if (vnode.children !== undefined) { for (j = 0; j < vnode.children.length; ++j) { i = vnode.children[j]; if (i != null && typeof i !== "string") { invokeDestroyHook(i); } } } } } function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number): void { for (; startIdx <= endIdx; ++startIdx) { let i: any, listeners: number, rm: () => void, ch = vnodes[startIdx]; if (ch != null) { if (isDef(ch.sel)) { invokeDestroyHook(ch); listeners = cbs.remove.length + 1; rm = createRmCb(ch.elm as Node, listeners); for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm); if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) { i(ch, rm); } else { rm(); } } else { // Text node api.removeChild(parentElm, ch.elm as Node); } } } } function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) { // 聲明指針 let oldStartIdx = 0, 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: any; let idxInOld: number; let elmToMove: VNode; let before: any; 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); // 將oldStart指向的節點插入到oldEnd指向節點前面 api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left 尾頭相同 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); // 將oldEnd指向節點插入到oldStart以前 api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (oldKeyToIdx === undefined) { // 製做key-index的哈西集合 oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = oldKeyToIdx[newStartVnode.key as string]; if (isUndef(idxInOld)) { // New element // 新節點則插入到oldStart指向的dom節點以前 api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node); } else { // 打補丁 // 對舊的位置座標記設爲undefined // 將更新後的節點移動到oldStart以前 patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined as any; api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node); } newStartVnode = newCh[++newStartIdx]; } } } if (oldStartIdx > oldEndIdx) {// 新的有剩餘則須要將這些節點插入到dom樹中 before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) {// 老的有節點則應該移除 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } } function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { let i: any, hook: any; // 調用prepatch鉤子 if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode); } const elm = vnode.elm = (oldVnode.elm as Node); let oldCh = oldVnode.children; let ch = vnode.children; if (oldVnode === vnode) return;// 沒有變化直接返回 if (vnode.data !== undefined) { // 對兩個虛擬dom樹的根節點進行更新 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); } if (isUndef(vnode.text)) { // 是否有文本節點 if (isDef(oldCh) && isDef(ch)) { // 都有子節點對子節點進行diff算法 if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue); } else if (isDef(ch)) { // 新虛擬dom有子節點舊的沒有,則把新的子節點掛在dom上 if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) {// 老的裏面有子節點,新的沒有直接幹掉老的 removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1); } else if (isDef(oldVnode.text)) { // 老的有文本信息直接設置爲空字符串 api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) {// 使用新的文本信息替換舊的 api.setTextContent(elm, vnode.text as string); } if (isDef(hook) && isDef(i = hook.postpatch)) { // 調用postpatch方法 i(oldVnode, vnode); } } return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; const insertedVnodeQueue: VNodeQueue = []; for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();// 調用全部模塊的pre鉤子方法 if (!isVnode(oldVnode)) { // 非虛擬dom直接清空 oldVnode = emptyNodeAt(oldVnode); } if (sameVnode(oldVnode, vnode)) { // 兩個虛擬dom樹的根節點徹底同樣纔會進行打補丁 patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 若是跟節點不一樣直接幹掉舊的根節點,從新建立dom元素插入到dom樹中 elm = oldVnode.elm as Node; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); if (parent !== null) { api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } // 對每一個插入的元素使用相應模塊中的insert鉤子方法進行更新 for (i = 0; i < insertedVnodeQueue.length; ++i) { (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]); } for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); // 調用全部鉤子模塊的post方法 return vnode; }; }
SnabbDOM的diff算法主要有兩個特色:app
同級比較的意思是,對於兩顆DOM樹,只會比較同一層級的節點,若是節點類型不一樣直接幹掉舊的節點,而不是繼續比較。那麼就近複用意味着若是節點類型相同就會對這個節點進行改造,而不是嚴格的比較各個屬性書否相同。那麼這裏涉及snabbdom中的兩個主要函數:dom
function patch(oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node; const insertedVnodeQueue: VNodeQueue = []; for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();// 調用全部模塊的pre鉤子方法 if (!isVnode(oldVnode)) { // 非虛擬dom直接清空 oldVnode = emptyNodeAt(oldVnode); } if (sameVnode(oldVnode, vnode)) { // 兩個虛擬dom樹的根節點徹底同樣纔會進行打補丁 patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 若是跟節點不一樣直接幹掉舊的根節點,從新建立dom元素插入到dom樹中 elm = oldVnode.elm as Node; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); if (parent !== null) { api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } // 對每一個插入的元素使用相應模塊中的insert鉤子方法進行更新 for (i = 0; i < insertedVnodeQueue.length; ++i) { (((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]); } for (i = 0; i < cbs.post.length; ++i) cbs.post[i](); // 調用全部鉤子模塊的post方法 return vnode; };
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { let i: any, hook: any; // 調用prepatch鉤子 if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) { i(oldVnode, vnode); } const elm = vnode.elm = (oldVnode.elm as Node); let oldCh = oldVnode.children; let ch = vnode.children; if (oldVnode === vnode) return;// 沒有變化直接返回 if (vnode.data !== undefined) { // 對兩個虛擬dom樹的根節點進行更新 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); i = vnode.data.hook; if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode); } if (isUndef(vnode.text)) { // 是否有文本節點 if (isDef(oldCh) && isDef(ch)) { // 都有子節點對子節點進行diff算法 if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue); } else if (isDef(ch)) { // 新虛擬dom有子節點舊的沒有,則把新的子節點掛在dom上 if (isDef(oldVnode.text)) api.setTextContent(elm, ''); addVnodes(elm, null, ch as Array<VNode>, 0, (ch as Array<VNode>).length - 1, insertedVnodeQueue); } else if (isDef(oldCh)) {// 老的裏面有子節點,新的沒有直接幹掉老的 removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1); } else if (isDef(oldVnode.text)) { // 老的有文本信息直接設置爲空字符串 api.setTextContent(elm, ''); } } else if (oldVnode.text !== vnode.text) {// 使用新的文本信息替換舊的 api.setTextContent(elm, vnode.text as string); } if (isDef(hook) && isDef(i = hook.postpatch)) { // 調用postpatch方法 i(oldVnode, vnode); } }
其中diff的核心算法就在updateChildren中
function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) { // 聲明指針 let oldStartIdx = 0, 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: any; let idxInOld: number; let elmToMove: VNode; let before: any; 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); // 將oldStart指向的節點插入到oldEnd指向節點前面 api.insertBefore(parentElm, oldStartVnode.elm as Node, api.nextSibling(oldEndVnode.elm as Node)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left 尾頭相同 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); // 將oldEnd指向節點插入到oldStart以前 api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (oldKeyToIdx === undefined) { // 製做key-index的哈西集合 oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = oldKeyToIdx[newStartVnode.key as string]; if (isUndef(idxInOld)) { // New element // 新節點則插入到oldStart指向的dom節點以前 api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node); } else { // 打補丁 // 對舊的位置座標記設爲undefined // 將更新後的節點移動到oldStart以前 patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined as any; api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node); } newStartVnode = newCh[++newStartIdx]; } } } if (oldStartIdx > oldEndIdx) {// 新的有剩餘則須要將這些節點插入到dom樹中 before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else if (newStartIdx > newEndIdx) {// 老的有節點則應該移除 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } }
這個方法有些複雜,下面文字描述未必那麼清晰,你們能夠看我錄製的視頻——虛擬DOM詳解。
下圖中白色圖表表明目前實際的DOM節點,oldStart和oldEnd指向以前的虛擬DOM樹,newStart和newEnd分別指向新的虛擬DOM節點。
如今兩者進行比較,首先處理頭頭、尾尾相同的節點,若是頭尾尾頭相同則同時移動新舊的指針
接下來處理頭尾尾頭相同的節點,把newStartIdx和oldEndIdx相同的節點插入到oldStartIdx指向節點以前,newStartIdx向後移動,oldEndIdx向前移動;把newEndIdx和oldStartIdx相同的節點插入到oldEndIdx指向的節點以後,同時oldStartIdx向後移動,newEndIdx向前移動。
處理完畢後的指針狀態:
接下來須要處理newStartIdx指向的節點11,那麼這時候先去oldStartIdx和oldEndIdx的區間內尋找有沒有這個節點,若是沒有那麼這個節點屬於插入節點,這個節點會被插入到oldStartIdx指向的節點的前面,同時newStartIdx向後移動
處理完11後,newStartIdx指向4,這個時候從oldStartIdx和oldEndIdx中可以找到這個節點,這說明它的位置被移動了,那麼這時候只須要移動這個節點,把它移動到oldStartIdx所指向的節點以前,同時對就的虛擬DOM節點的位置進行標記,這裏是設置爲undefined。繼續移動newStartIdx。
接下來對七、 五、 6 都進行相同操做,這時候newStartIdx指向3的位置
那麼這個時候又變成了頭頭相同的狀況,只須要同時將newStartIdx和oldStartIdx向後移動。
那麼這時候newStartIdx越過了newEndIdx,到這裏循環結束,這時候oldStartIdx和oldEndIdx中剩下的節點都是須要刪除的節點。由於以前都已經打上了標記,因此這裏只要節點8是須要刪除的。固然也有時候會遇到oldStartIdx和oldEndIdx先相遇,這時候在newStartIdx和newEndIdx中的節點都是須要插入的。
至此整個比較算法結束。
下面是我錄製的視頻,有動畫的能夠更清晰的展現這個過程。