虚拟DOM简介

  Virtual Dom可以看做一棵模拟了DOM树的JavaScript对象树,其主要是通过vnode,实现一个无状态的组件,当组件状态发生更新时,然后触发Virtual Dom数据的变化,然后通过Virtual Dom和真实DOM的比对,再对真实DOM更新。虚拟DOM其实就是一种模拟DOM的JavaScript数据结构。

  像SnabbDOM这种库的虚拟DOM是如下的数据结构:

  • sel 元素选择器
  • data 元素属性 ●
  • children 元素子节点 ●
  • text 元素文本 ●
  • elm 对应dom元素 ●
  • key

SnabbDOM源码概述

  说到SnabbDOM可能大家不太知道,但是大名鼎鼎的VUE就是使用SnabbDOM来提供虚拟DOM。SnabbDOM中的VNode结构如下:

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:

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

  那么在具体使用场景如下:

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目录下的各个文件中。

  其中核心代码,包括整个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;
};
}

Diff算法

  SnabbDOM的diff算法主要有两个特点:

  • 同级比较
  • 就近复用

  

  同级比较的意思是,对于两颗DOM树,只会比较同一层级的节点,如果节点类型不同直接干掉旧的节点,而不是继续比较。那么就近复用意味着如果节点类型相同就会对这个节点进行改造,而不是严格的比较各个属性书否相同。那么这里涉及snabbdom中的两个主要函数:

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。

  接下来对7、 5、 6 都进行相同操作,这时候newStartIdx指向3的位置

  那么这个时候又变成了头头相同的情况,只需要同时将newStartIdx和oldStartIdx向后移动。

  那么这时候newStartIdx越过了newEndIdx,到这里循环结束,这时候oldStartIdx和oldEndIdx中剩下的节点都是需要删除的节点。因为之前都已经打上了标记,所以这里只要节点8是需要删除的。当然也有时候会遇到oldStartIdx和oldEndIdx先相遇,这时候在newStartIdx和newEndIdx中的节点都是需要插入的。

  至此整个比较算法结束。

参考资料

深入 Vue2.x 的虚拟 DOM diff 原理

vue的Virtual Dom实现snabbdom解密

snabbdom

vue中Virtual DOM源码学习

下面是我录制的视频,有动画的可以更清晰的展示这个过程。

虚拟DOM详解的更多相关文章

  1. 虚拟Dom详解 - (二)

    第一篇文章中主要讲解了虚拟DOM基本实现,简单的回顾一下,虚拟DOM是使用json数据描述的一段虚拟Node节点树,通过render函数生成其真实DOM节点.并添加到其对应的元素容器中.在创建真实DO ...

  2. 虚拟Dom详解 - (一)

    随着Vue和React的风声水起,伴随着诸多框架的成长,虚拟DOM渐渐成了我们经常议论和讨论的话题.什么是虚拟DOM,虚拟DOM是如何渲染的,那么Vue的虚拟Dom和React的虚拟DOM到底有什么区 ...

  3. brctl创建虚拟网卡详解

    brctl创建虚拟网卡详解 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 很久之前我分享过一篇关于搭建Openvpn的笔记,在笔记的最后我分享了一个脚本,是用来创建虚拟网卡的,今天 ...

  4. Day04 dom详解及js事件

    day04 dom详解 DOM的基础 Document对象 Element对象 Node对象 innerHTML 事件处理 表单验证   上次课内容回顾: JS中ECMAScript用法: JS定义变 ...

  5. JavaScript(2)---DOM详解

    JavaScript(2)---DOM详解 一.DOM概念 什么是DOM DOM全称为文本对象模型(Document Object Model),它定义了所有HTML元素的对象和属性,以及访问他们的方 ...

  6. JavaScript进阶内容——DOM详解

    JavaScript进阶内容--DOM详解 当我们已经熟练掌握JavaScript的语法之后,我们就该进入更深层次的学习了 首先我们思考一下:JavaScript是用来做什么的? JavaScript ...

  7. Signalr系列之虚拟目录详解与应用中的CDN加速实战

    目录 对SignalR不了解的人可以直接移步下面的目录 SignalR系列目录 前言 前段时间一直有人问我 在用SignalR 2.0开发客服系统[系列1:实现群发通讯]这篇文章中的"/Si ...

  8. Linux 虚拟网络设备详解之 Bridge 网桥

    本文首发于我的公众号 Linux云计算网络(id: cloud_dev),专注于干货分享,号内有 10T 书籍和视频资源,后台回复「1024」即可领取,欢迎大家关注,二维码文末可以扫. 前面几篇文章介 ...

  9. XML解析之DOM详解及与SAX解析方法的比较

    XML解析(DOM) XML文件解析方法介绍 我们所用到的NSXMLParser是采用SAX方法解析 SAX(Simple API for XML) 只能读,不能修改,只能顺序访问,适合解析大型XML ...

随机推荐

  1. MicroService.Core简易微服务框架《一、简介》

    MicroService.Core MicroService.Core 的初衷是为了方便的创建一个微服务, 可作为 Windows Service 或者控制台模式启动. 它底层使用了 OWin 自托管 ...

  2. 1031: [JSOI2007]字符加密Cipher

    1031: [JSOI2007]字符加密Cipher Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 7338  Solved: 3182[Submit ...

  3. 0_Simple__simpleAtomicIntrinsics + 0_Simple__simpleAtomicIntrinsics_nvrtc

    原子操作.并且在静态代码和运行时编译两种条件下使用. ▶ 源代码:静态使用 #ifndef _SIMPLEATOMICS_KERNEL_H_ #define _SIMPLEATOMICS_KERNEL ...

  4. PgSql on Docker with HaProxy 反向代理

    Run PgSql on Docker as Remote Db docker run -d -it \ --name pgdb \ -p 5432:5432 \ -e POSTGRES_USER=p ...

  5. eclipse安装checkstyle无法加载到preferences的问题

    描述一下问题,eclipse安装checkstyle,不管是在线安装还是下载安装,在preferences都没有checkstyle选项,如下: 然我们要的效果是这样的:   解决方案如下: 1 启动 ...

  6. C++雾中风景1:友元类与面向对象

    因为后续准备入职的公司都希望能转C++,所以最近也是按部就班的开始进行C++的学习.然后这个系列的文章打算探究C++的语言特性,也比较一下不同语言(如Java,Scala,Python,Go)之间的设 ...

  7. js贪吃蛇-简单版

    分享个用原生js写的贪吃蛇,最近在学java,按照当年写的 js的思路,转换成java,换汤不换药 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1 ...

  8. Spring+Spring MVC+MyBatis框架集成

    目录 一.新建一个基于Maven的Web项目 二.创建数据库与表 三.添加依赖包 四.新建POJO实体层 五.新建MyBatis SQL映射层 六.JUnit测试数据访问 七.完成Spring整合My ...

  9. SpringMVC接收复杂对象

    SpringMVC接收复杂对象 转载请注明地址:http://www.cnblogs.com/funnyzpc/p/7642977.html 本节内容暂放一边,我先扯点儿,心情好了,代码顺风顺水哈~ ...

  10. OpenTSDB - 分布式可扩展的监控系统

    OpenTSDB - A Distributed, Scalable Monitoring System http://opentsdb.net/getting-started.html http:/ ...