iCMS

虚拟DOM详解

好喜欢专业转载优质博客文章的地方哦~

虚拟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:

虚拟DOM详解

虚拟DOM详解

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,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 = ; }

else { data = b; }

}

if (is.array(children)) {

for (i = 0; i < children.length; ++i) {

if (is.primitive(children[i])) children[i] = vnode(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,text,undefined);

};

View Code

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

虚拟DOM详解

虚拟DOM详解

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)

])

)),

]);

View Code

其中跟虚拟DOM各个周期中需要使用的hook方法都放在Hook.ts这个文件下:

虚拟DOM详解

虚拟DOM详解

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;

}

View Code

具体的钩子方法则是放在src/modules目录下的各个文件中。

其中核心代码,包括整个Diff算法都放在snabbdom.ts文件中:

虚拟DOM详解

虚拟DOM详解

/* 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);

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,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,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,

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,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,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,oldStartVnode.elm as Node);

newStartVnode = newCh[++newStartIdx];

} else {

elmToMove = oldCh[idxInOld];

if (elmToMove.sel !== newStartVnode.sel) {

api.insertBefore(parentElm,oldStartVnode.elm as Node);

} else {

// 打补丁

// 对旧的位置坐标记设为undefined

// 将更新后的节点移动到oldStart之前

patchVnode(elmToMove,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,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>).length - 1,insertedVnodeQueue);

} else if (isDef(oldCh)) {// 老的里面有子节点,新的没有直接干掉老的

removeVnodes(elm,(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);

}

}

// 对每个插入的元素使用相应模块中的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;

};

}

View Code

Diff算法

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

同级比较

就近复用

虚拟DOM详解

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

function patch(oldVnode: VNode | Element,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);

}

}

其中diff的核心算法就在updateChildren中

function updateChildren(parentElm: Node,oldEndIdx);

}

}

这个方法有些复杂,下面文字描述未必那么清晰,[b]大家可以看我录制的视频——虚拟DOM详解。

下图中白色图表代表目前实际的DOM节点,oldStart和oldEnd指向之前的虚拟DOM树,newStart和newEnd分别指向新的虚拟DOM节点。

虚拟DOM详解

现在二者进行比较,首先处理头头、尾尾相同的节点,如果头尾尾头相同则同时移动新旧的指针

虚拟DOM详解

接下来处理头尾尾头相同的节点,把newStartIdx和oldEndIdx相同的节点插入到oldStartIdx指向节点之前,newStartIdx向后移动,oldEndIdx向前移动;把newEndIdx和oldStartIdx相同的节点插入到oldEndIdx指向的节点之后,同时oldStartIdx向后移动,newEndIdx向前移动。

虚拟DOM详解

处理完毕后的指针状态:

虚拟DOM详解

接下来需要处理newStartIdx指向的节点11,那么这时候先去oldStartIdx和oldEndIdx的区间内寻找有没有这个节点,如果没有那么这个节点属于插入节点,这个节点会被插入到oldStartIdx指向的节点的前面,同时newStartIdx向后移动

虚拟DOM详解

处理完11后,newStartIdx指向4,这个时候从oldStartIdx和oldEndIdx中能够找到这个节点,这说明它的位置被移动了,那么这时候只需要移动这个节点,把它移动到oldStartIdx所指向的节点之前,同时对就的虚拟DOM节点的位置进行标记,这里是设置为undefined。继续移动newStartIdx。

虚拟DOM详解

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

虚拟DOM详解

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

虚拟DOM详解

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

至此整个比较算法结束。

参考资料

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

vue的Virtual Dom实现snabbdom解密

snabbdom

vue中Virtual DOM源码学习

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

0

上一篇:

:下一篇

精彩评论

暂无评论...
验证码 换一张
取 消

热门标签