前言
随着react、vue等前端框架崛起,虚拟DOM
也被越来越多的人悉知。虚拟DOM 一个听起来好高大上的词,貌似“深不可测”的样子。但当你去深入了解之后,会发现虚拟DOM并没有那么神秘,也不是那么难以理解。很多时候只是不知道从何处下手而已。
snabbdom
就是虚拟DOM的一个简洁实现。置于为什么选择snabbdom
,诚如官方文档所说snabbdom:
- 主体代码只有200多行,简单易读且性能优越
- 功能强大、扩展性强
- 丰富的钩子函数
并且vue2.x中的Virtual DOM部分也是基于snabbdom实现的。学习snabbdom能让你更好的理解什么是虚拟dom之外,也能更有助于你去读懂vue源码。如果你没听过或没用过snabbdom,建议你先去看看官方文档。
关于Virtual Dom
什么是Virtual Dom:Virtual dom是真实DOM的抽象,可以理解为一个纯js对象。这个对象只保存了dom必要的信息。
Virtual dom能很好的描述一个dom结构。操作一个轻量的js对象要远比操作dom快的多,且比较前后两个对象的不同来更新dom可以避免重复操作未变更的dom,同时也能更好的检测数据变化来更新dom。
Virtual dom的实现都会有两个过程:
- 根据描述dom的js对象创建出真正的dom树,再应用到文档上。
- 数据变化时,比较旧js和新js对象,得到对象之间的差异,更新dom。(diff算法)
但是对于Virtual Dom不要一味的认为就是快,快也要分场合;要分清楚初始渲染、小量数据更新、大量数据更新这些不同的场合。
- 初始渲染:Virtual DOM > 脏检查 >= 依赖收集
- 小量数据更新:依赖收集 >> Virtual DOM + 优化 > 脏检查(无法优化) > Virtual DOM 无优化
- 大量数据更新:脏检查 + 优化 >= 依赖收集 + 优化 > Virtual DOM(无法/无需优化)>> MVVM 无优化
有时对dom操作的优化能达到比Virtual dom更好的性能。Virtual Dom更实际的意义是:
- 为函数式的 UI 编程方式打开了大门;
- 可以渲染到 DOM 以外的 渲染后端。
这里是借用了尤大的回答,具体可以看看以下两篇回答内容
网上都说操作真实 DOM 慢,但测试结果却比 React 更快,为什么?
RxJS/Cycle.js 与 React/Vue 相比更适用于什么样的应用场景?
源码分析
由于源码是使用TypeScript写的,不是很熟悉的先去看看TypeScript。
阅读源码首先要找到切入点,也就是入口。snabbdom的主要方法也就几个,分别是init
、h
、patch
、tovnode
。顾名思义init就是我们要的切入点。结合例子来看能更好的读懂源码,我们先看看官方给的例子
var snabbdom = require('snabbdom');
var patch = snabbdom.init([ // Init patch function with chosen modules
require('snabbdom/modules/class').default, // makes it easy to toggle classes
require('snabbdom/modules/props').default, // for setting properties on DOM elements
require('snabbdom/modules/style').default, // handles styling on elements with support for animations
require('snabbdom/modules/eventlisteners').default, // attaches event listeners
]);
var h = require('snabbdom/h').default; // helper function for creating vnodes
var container = document.getElementById('container');
var 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 into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
var 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!')
]);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
// to unmount from the DOM and clean up, simply pass null
patch(newVnode, null)
例子很简单,就是snabbdom
调用init
方法,传入一个包含模块的数组,有class
、props
、style
、eventlisteners
这几个内置模块,当然你也可以添加自己扩展的模块。最后返回一个patch
。接着调用h
函数来生成vnode
。之后调用patch
函数更新dom。patch
接收两个参数,第一个参数是旧的vnode
对象,第二参数是新的vnode
对象。patch
可以根据两个对象的差异更新dom。
init 函数
init
函数在源码的src/snabbdom.ts
文件。源码如下
// 模块实现钩子函数的key(hook key)
const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post'];
export function init(modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number, j: number, cbs = ({} as ModuleHooks);
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
// 对模块内定义的钩子函数合并到一个对象中。{create:[fn, fn, ...], update: [...], ...}
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) {
// ...
}
function createRmCb(childElm: Node, listeners: number) {
// ...
}
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
// ...
}
function addVnodes(parentElm: Node, before: Node | null, vnodes: Array<VNode>, startIdx: number, endIdx: number, insertedVnodeQueue: VNodeQueue
) {
// ...
}
function invokeDestroyHook(vnode: VNode) {
// ...
}
function removeVnodes(parentElm: Node, vnodes: Array<VNode>, startIdx: number, endIdx: number): void {
// ...
}
function updateChildren(parentElm: Node, oldCh: Array<VNode>, newCh: Array<VNode>, insertedVnodeQueue: VNodeQueue) {
// ...
}
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// ...
}
return function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
// ...
};
}
init
传入两个参数,第一个是模块数组,第二个参数是domApi
,是可选的。
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi;
可以看到如果没有传入domApi
这个参数,domApi默认是htmlDomApi
。htmlDomApi
就是DOM原生的一些操作dom的api。在src/htmldomapi.ts
文件。
init
方法里还定义了一系列的方法,这些方法是一些对VNode
的diff
以及创建真实dom
和hook
相关的函数,这里先不分析;直接看到init
最后是返回了一个patch函数。
先来看看h
函数
h 函数
export function h(sel: string): VNode;
export function h(sel: string, data: VNodeData): VNode;
export function h(sel: string, children: VNodeChildren): VNode;
export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode;
export function h(sel: any, b?: any, c?: any): VNode {
var data: VNodeData = {}, children: any, text: any, i: number;
// c主要是格式化children属性,c如果是数组则赋值给children
// 如果是‘string’类型或‘number’类型,则赋值给text
// 如果c含有sel属性则转成数组
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]; }
// b主要是格式化data属性,逻辑同上
} 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 (children !== undefined) {
for (i = 0; i < children.length; ++i) {
// 如果children里面的项是文本或数值,则都转成vnode对象
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined);
}
}
// 针对svg的处理
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);
}
h
函数的使用方式有很多种,以上代码就是对传入的参数进行格式化,最终返回一个VNode
对象
h
函数里用到vnode
方法定义在src/vnode.ts
文件里。同时我们也可以看看snabbdom
是如何定义一个Virtual dom对象的
export interface VNode {
sel: string | undefined; // 选择器
data: VNodeData | undefined; // 描述dom的对象
children: Array<VNode | string> | undefined; // 子节点
elm: Node | undefined; // 真实dom元素的引用
text: string | undefined; // dom字体文本
key: Key | undefined; // 用于diff时提升性能的key
}
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
}
// vnode方法返回VNode对象
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, data, children, text, elm, key};
}
接下来就来看看在init
定义的一些方法,先来看createElm
函数
createElm 函数
// 这个函数是创建真实的dom元素
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any, data = vnode.data;
// 调用h函数时传入的对象,即第二个参数。
// 如果传入init钩子函数,则在dom创建前调用
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);
// 设置id、class属性
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot));
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '));
// 调用模块的create钩子函数
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)) {
// 调用该节点传入create钩子函数
if (i.create) i.create(emptyNode, vnode);
// 如果有insert钩子函数,则填充insertedVnodeQueue数组,避免再次遍历
if (i.insert) insertedVnodeQueue.push(vnode);
}
} else {
// sel为undefined,则创建文本节点
vnode.elm = api.createTextNode(vnode.text as string);
}
return vnode.elm;
}
可以看到createElm
节点的逻辑也是比较简单的。先调用了元素的init钩子函数,接着判断sel选择器
- 如果
sel
是!
,则创建注释节点,赋值给vnode.elm(下面两种情况同样会赋值给vnode.elm) - 如果
sel
不是undefined
,即有值,这会对sel
解析,取得标签tag,如果获取得到id和class,则会调用setAttribute
设置id和class。如果有子节点则递归调用。之后如果存在hook,则触发create hook
;用insertedVnodeQueue
存储insert hook
,在patch
时触发。 - 如果
sel
获取不到,则创建文本节点。
最后返回vnode.elm
addVnodes 函数
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) {
// 创建节点并插入到before节点前
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before);
}
}
}
addVnodes
函数就是用来插入节点的
emptyNodeAt 函数
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);
}
emptyNodeAt
函数就是把传入elem
对象转成空的VNode
对象,这个对象中只保留了Element
的tag、id、class
createRmCb 函数
function createRmCb(childElm: Node, listeners: number) {
return function rmCb() {
if (--listeners === 0) {
const parent = api.parentNode(childElm);
api.removeChild(parent, childElm);
}
};
}
createRmCb
函数作用是移除dom元素。在removeVnodes
函数会调用。createRmCb
函数里有if (--listeners === 0)
这个判定条件,这个是针对模块钩子函数的判定条件。只有当所有的remove hook调用完了,才会移除dom。
invokeDestroyHook 函数
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);
}
}
}
}
}
顾名思义invokeDestroyHook
这个函数就是在dom销毁前触发destroy hook的。
removeVnodes 函数
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)) {
// 先触发destroy hook
invokeDestroyHook(ch);
listeners = cbs.remove.length + 1;
rm = createRmCb(ch.elm as Node, listeners);
// 触发模块的remove hook
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);
}
}
}
}
removeVnodes
也很简单,就是移除dom
看完上面那些函数现在就来看看Snabbdom
的核心函数了
patch 函数
function patch(oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node;
// 定义insert队列数组,用于存储insert hook
const insertedVnodeQueue: VNodeQueue = [];
// 调用模块(module )的 pre 钩子函数
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
// 如果传入的是 Element,这个转成空的VNode对象
if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode);
}
// 相同的VNode对象(sel和key相同则认为相同),则调用patchVnode
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue);
} else {
elm = oldVnode.elm as Node;
parent = api.parentNode(elm);
// 不相同,则创建新的dom
createElm(vnode, insertedVnodeQueue);
if (parent !== null) {
// 插入新的dom节点
api.insertBefore(parent, vnode.elm as Node, api.nextSibling(elm));
// 移除旧的dom节点
removeVnodes(parent, [oldVnode], 0, 0);
}
}
// 调用insert hook
// 从以下代码可以看出,以及init hook合并中,可以看出模块是不支持insert hook的
for (i = 0; i < insertedVnodeQueue.length; ++i) {
(((insertedVnodeQueue[i].data as VNodeData).hook as Hooks).insert as any)(insertedVnodeQueue[i]);
}
// 调用模块的 post 钩子函数
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
return vnode;
}
patch
函数就是init最终返回的函数,也是能更新dom的函数,是Snabbdom
所暴露的核心函数。patch里面的逻辑还是容易理解的
patchVnode 函数
patchVnode
是Snabbdom
最核心的函数,也是Virtual DOM
的核心,负责VNode
的diff
,并将差异更新到dom上。
在patchVnode
里还调用了另一个核心函数:updateChildren
函数。这个函数也是做diff
处理的,处理的是VNode
复杂的部分,也是Virtual DOM
比较复杂的部分。
updateChildren
暂时先不管,先来看看patchVnode
是怎么实现diff
的
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);
}
// elm指向vnode.elm和oldVnode.elm
const elm = vnode.elm = (oldVnode.elm as Node);
let oldCh = oldVnode.children;
let ch = vnode.children;
// 节点相同,结束patchVnode不做后续处理
if (oldVnode === vnode) return;
if (vnode.data !== undefined) {
// 调用模块的update hook
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
i = vnode.data.hook;
// 调用元素的update hook
if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode);
}
// 新节点不是文本节点
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 新旧节点都存在children节点且不相等,调用updateChildren进行diff更新
if (oldCh !== ch) updateChildren(elm, oldCh as Array<VNode>, ch as Array<VNode>, insertedVnodeQueue);
} else if (isDef(ch)) {
// 新节点有children,旧节点没有children
// 则先移除旧的文本节点,再插入新的vnode作为children节点
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)) {
// 旧节点有children,新节点没有,则直接移除旧节点的children
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
} else if (isDef(oldVnode.text)) {
// 以上情况都不满足,且旧节点有text,则置空
api.setTextContent(elm, '');
}
} else if (oldVnode.text !== vnode.text) {
// 新节点是文本,且文本内容不同。并且旧节点含有子节点,
// 则删除旧节点的子节点,同时更新text文本
if (isDef(oldCh)) {
removeVnodes(elm, oldCh as Array<VNode>, 0, (oldCh as Array<VNode>).length - 1);
}
api.setTextContent(elm, vnode.text as string);
}
// 调用元素的postpatch hook
if (isDef(hook) && isDef(i = hook.postpatch)) {
i(oldVnode, vnode);
}
}
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语法,判定条件是新旧的startIndex都要小于endIndex,每次只处理一个节点
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 若果oldStartVnode该节点不存在则跳过继续下一个节点,下面三个判断同理
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更新,继续循环比较下一个节点
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 如果新旧节点的末尾节点相同则调用patchVnode更新,继续循环比较上一个节点
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
// 参考内容来自:https://juejin.cn/post/6844903671906435080#heading-8
// 旧开始节点等于新的结束节点,说明节点向右移动了
// 至于为什么是向右移:oldStartVnode 和 newEndVnode 相同,当然是 vnode 右移了
// 具体移动到哪,因为新节点处于末尾,所以添加到旧结束节点的后面,旧节点会随着 updateChildren 的调用向左移
// 注意这里需要移动 dom,因为节点右移了,而为什么是插入 oldEndVnode 的后面呢?
// 可以分为两个情况来理解:
// 1. 当循环刚开始,下标都还没有移动,那移动到 oldEndVnode 的后面就相当于是最后面,是合理的
// 2. 循环已经执行过一部分了,因为每次比较结束后,下标都会向中间靠拢,而且每次都会处理一个节点,
// 这时下标左右两边已经处理完成,可以把下标开始到结束区域当成是并未开始循环的一个整体,
// 所以插入到 oldEndVnode 后面是合理的(在当前循环来说,也相当于是最后面,同 1)
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
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);
api.insertBefore(parentElm, oldEndVnode.elm as Node, oldStartVnode.elm as Node);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
// 若果以上条件都不满足,则根据key来做判断,
// 那么这个vnode有可能是在中间的某个位置,
// 也有节点可能是新创建的节点
} else {
// 如果没有oldKeyToIdx,则创建oldCh的key到index的map映射
// 以便于通过key拿到对应的index下标,从而获取到对应的旧节点
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 通过新vnode的key去拿在oldCh下对应的下标
idxInOld = oldKeyToIdx[newStartVnode.key as string];
// 下标不存在,则说明是vnode是全新的
if (isUndef(idxInOld)) { // New element
// 把新的dom插入到oldStartVnode前面
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
newStartVnode = newCh[++newStartIdx];
} else {
// 下标存在,取到oldCh对应的vnode
elmToMove = oldCh[idxInOld];
// 即使下标存在但sel不同,也创建新的节点并插入到oldStartVnode前面
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm as Node);
} else {
// 节点相同,调用patchVnode更新
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
// 该节点已经处理过了,把这个节点置空,当下次循环到这个节点时跳过
oldCh[idxInOld] = undefined as any;
// 把这个节点插入到oldStartVnode前面
api.insertBefore(parentElm, (elmToMove.elm as Node), oldStartVnode.elm as Node);
}
newStartVnode = newCh[++newStartIdx];
}
}
}
// 当循环结束后,有可能存在还未处理的vnode。
// 因为oldCh和和新的newCh数组长度不可能总是相同的;
// 这里有两种情况:
// 1. 旧vnode处理完了,新vnode还有没处理完的
// 2. 新vnode的处理完了,旧vnode的还有没处理完的
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
// 说明oldCh已经先处理完了,还有新的vnode没有处理完,则插入剩下的vnode到dom
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
// oldCh有多余的vnode,则删除多余的dom(vnode对应的dom)
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
}
看完updateChildren
可以知道,对于新旧vnode的diff策略是:
第一步比较初始vnode
是否相同,相同则patchVnode
;第二步在在比较末尾vnode
是否相同;第三步是初始
和末尾
交叉比较;最后是通过key
来处理
如果对于Vnode
节点移动不是很理解的可以看看这个两篇博客的图解:
最后
到这里snabbdom
的核心实现就分析完了。还有thunk
函数和一些内置modules
可以自行阅读。可以发现,Virtual DOM 并没有我们想的那么复杂。
其它diff
相关的文章:不可思议的 react diff