snabbdom是一个Virtual-DOM的实现库,它专一于使用的简单以及功能和的模型化,并在效率和性能上有着很好的表现。若是你还不知道什么是Virtual-DOM技术,它是一种网页中经过diff算法来实现网页修改最小化的方法,react底层使用了这样的机制来提升性能。javascript
从Vue2发布开始,也开始使用了这样的机制。Vue并无选择本身从新造一套Virtual-DOM的算法,而是在snabbdom的基础上构建了一个嵌入了框架自己的fork版本。能够说,Vue就是在使用snabbdom的Virtual-DOM算法。java
initnode
snabbdom使用一种相似于插件声明使用的方式来模块化功能,若是你使用过AngularJS的声明注入或者Vue.use,你对这样的方式必定不陌生。react
var patch = snabbdom.init([
require('snabbdom/modules/class').default,
require('snabbdom/modules/style').default,
]);
复制代码
patch算法
patch是由init返回的一个函数,第一个参数表明着以前的view,是一个vnode或者DOM节点,而第二个参数是一个新的vnode节点,oldNode会根据他的类型被相应的更新。编程
patch(oldVnode, newVnode);
复制代码
h函数api
h函数可让你更加轻松的创建vnode。框架
var snabbdom = require('snabbdom')
var patch = snabbdom.init([ // 调用init生成patch
require('snabbdom/modules/class').default, // 让toggle class更加简单
require('snabbdom/modules/props').default, // 让DOM能够设置props
require('snabbdom/modules/style').default, // 支持带有style的元素,以及动画
require('snabbdom/modules/eventlisteners').default, // 加上事件监听
]);
var h = require('snabbdom/h').default; // h的意思是helper,帮助创建vnode
var toVNode = require('snabbdom/tovnode').default;
var newNode = h('div', {style: {color: '#000'}}, [
h('h1', 'Headline'),
h('p', 'A paragraph'),
]);
patch(toVNode(document.querySelector('.container')), newVNode)
复制代码
钩子(hook)dom
名称 | 触发时间 | 回调参数 |
---|---|---|
pre |
patch开始 | none |
init |
vnode被添加的时候 | vnode |
create |
DOM元素被从create建立 | emptyVnode, vnode |
insert |
一个元素被插入了DOM | vnode |
prepatch |
元素即将被patch | oldVnode, vnode |
update |
元素被更新 | oldVnode, vnode |
postpatch |
元素被patch后 | oldVnode, vnode |
destroy |
元素被直接或者间接移除 | vnode |
remove |
元素直接从DOM被移除 | vnode, removeCallback |
post |
patch操做结束 | none |
diff两棵树的算法是一个O(n^3)的算法模块化
对于两个元素,若是他们类型不一样,或者key不一样,那么元素就不是同一个元素,那么直接新的元素替换前一个元素。
对于两个元素是同一个元素的状况下,开始diff他们的附加元素,还有他们的children。
snabbdom在diff他们的children时候,一次性对比四个节点,oldNode与newNode的Children的首尾元素:
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 开头处理了边界状况和特殊状况
if (oldStartVnode == null) {
// 若是oldStartVnode为空,那么日后移动继续探测
oldStartVnode = oldCh[++oldStartIdx];
} else if (oldEndVnode == null) {
// 若是oldEndVnode为空,那么往前移动继续探测
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];
// 如今的首节点相同,diff他们两个的其余属性,而且start接着日后走
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
// 如今的尾节点相同,diff他们两个的其余属性,而且old接着往前走
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
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];
// 首尾相同的状况,对旧的节点调整孩子顺序,并继续分别收缩范围
} else {
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
}
// 使用这里实现了Key和Index的对应索引
idxInOld = oldKeyToIdx[newStartVnode.key as string];
if (isUndef(idxInOld)) { // 这是一个新的元素
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 {
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 || newStartIdx <= newEndIdx) {
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm;
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
} else {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
}
}
复制代码
经过对于index与key的对应,以及特殊状况的对应,使diff算法的平均状况可以达到O(nlogn)。
并且根据init的注入,diff的内容还能够选择性的加入不一样内容,来优化性能。