vue在官方文档中提到与react的渲染性能对比中,因为其使用了snabbdom而有更优异的性能。
JavaScript 开销直接与求算必要 DOM 操作的机制相关。尽管 Vue 和 React 都使用了 Virtual Dom 实现这一点,但 Vue 的 Virtual Dom 实现(复刻自 snabbdom)是更加轻量化的,因此也就比 React 的实现更高效。
看到火到不行的国产前端框架vue也在用别人的 Virtual Dom开源方案,是不是很好奇snabbdom有何强大之处呢?不过正式解密snabbdom之前,先简单介绍下Virtual Dom。
什么是Virtual Dom
Virtual Dom可以看做一棵模拟了DOM树的JavaScript树,其主要是通过vnode,实现一个无状态的组件,当组件状态发生更新时,然后触发Virtual Dom数据的变化,然后通过Virtual Dom和真实DOM的比对,再对真实DOM更新。可以简单认为Virtual Dom是真实DOM的缓存。
为什么用Virtual Dom
我们知道,当我们希望实现一个具有复杂状态的界面时,如果我们在每个可能发生变化的组件上都绑定事件,绑定字段数据,那么很快由于状态太多,我们需要维护的事件和字段将会越来越多,代码也会越来越复杂,于是,我们想我们可不可以将视图和状态分开来,只要视图发生变化,对应状态也发生变化,然后状态变化,我们再重绘整个视图就好了。
这样的想法虽好,但是代价太高了,于是我们又想,能不能只更新状态发生变化的视图?于是Virtual Dom应运而生,状态变化先反馈到Virtual Dom上,Virtual Dom在找到最小更新视图,最后批量更新到真实DOM上,从而达到性能的提升。
除此之外,从移植性上看,Virtual Dom还对真实dom做了一次抽象,这意味着Virtual Dom对应的可以不是浏览器的DOM,而是不同设备的组件,极大的方便了多平台的使用。如果是要实现前后端同构直出方案,使用Virtual Dom的框架实现起来是比较简单的,因为在服务端的Virtual Dom跟浏览器DOM接口并没有绑定关系。

初始渲染时,首先将数据渲染为 Virtual DOM,然后由 Virtual DOM 生成 DOM。

数据更新时,渲染得到新的 Virtual DOM,与上一次得到的 Virtual DOM 进行 diff,得到所有需要在 DOM 上进行的变更,然后在 patch 过程中应用到 DOM 上实现UI的同步更新。
Virtual DOM 作为数据结构,需要能准确地转换为真实 DOM,并且方便进行对比。
介绍完Virtual DOM,我们应该对snabbdom的功用有个认识了,下面具体解剖下snabbdom这只“小麻雀”。
snabbdom
vnode


DOM 通常被视为一棵树,元素则是这棵树上的节点(node),而 Virtual DOM 的基础,就是 Virtual Node 了。
Snabbdom 的 Virtual Node 则是纯数据对象,通过 vnode 模块来创建,对象属性包括:
可以看到 Virtual Node 用于创建真实节点的数据包括: 属性
元素的子节点
源码: var key = data === undefined ? undefined : data.key; snabbdom并没有直接暴露vnode对象给我们用,而是使用h包装器,h的主要功能是处理参数: vnode
从snabbdom的typescript的源码可以看出,其实就是这几种函数重载: patch 创建vnode后,接下来就是调用patch方法将Virtual Dom渲染成真实DOM了。patch是snabbdom的init函数返回的。
snabbdom.init传入modules数组,module用来扩展snabbdom创建复杂dom的能力。 不多说了直接上patch的源码: createElm(vnode,insertedVnodeQueue); if (parent !== null) { 先判断新旧虚拟dom是否是相同层级vnode,是才执行patchVnode,否则创建新dom删除旧dom,判断是否相同vnode比较简单: patch方法里面实现了snabbdom 作为一个高效virtual dom库的法宝—高效的diff算法,可以用一张图示意:
return { sel: sel,data: data,children: children,text: text,elm: elm,key: key };
}
api.insertBefore(parent,vnode.elm,api.nextSibling(elm));
removeVnodes(parent,[oldVnode],0);
}
}
//插入完后,调用被插入的vnode的insert钩子
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]);
}
//然后调用全局下的post钩子
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
//返回vnode用作下次patch的oldvnode
return vnode;
};
diff算法的核心是比较只会在同层级进行,不会跨层级比较。而不是逐层逐层搜索遍历的方式,时间复杂度将会达到 O(n^3)的级别,代价非常高,而只比较同层级的方式时间复杂度可以降低到O(n)。
patchVnode函数的主要作用是以打补丁的方式去更新dom树。
<div class="jb51code">
<pre class="brush:js;">
function patchVnode(oldVnode,insertedVnodeQueue) {
var i,hook;
//在patch之前,先调用vnode.data的prepatch钩子
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
i(oldVnode,vnode);
}
var elm = vnode.elm = oldVnode.elm,oldCh = oldVnode.children,ch = vnode.children;
//如果oldvnode和vnode的引用相同,说明没发生任何变化直接返回,避免性能浪费
if (oldVnode === vnode) return;
//如果oldvnode和vnode不同,说明vnode有更新
//如果vnode和oldvnode不相似则直接用vnode引用的DOM节点去替代oldvnode引用的旧节点
if (!sameVnode(oldVnode,vnode)) {
var parentElm = api.parentNode(oldVnode.elm);
elm = createElm(vnode,insertedVnodeQueue);
api.insertBefore(parentElm,oldVnode.elm);
removeVnodes(parentElm,0);
return;
}
//如果vnode和oldvnode相似,那么我们要对oldvnode本身进行更新
if (isDef(vnode.data)) {
//首先调用全局的update钩子,对vnode.elm本身属性进行更新
for (i = 0; i < cbs.update.length; ++i) cbs.updatei;
//然后调用vnode.data里面的update钩子,再次对vnode.elm更新
i = vnode.data.hook;
if (isDef(i) && isDef(i = i.update)) i(oldVnode,vnode);
}
//如果vnode不是text节点
if (isUndef(vnode.text)) {
//如果vnode和oldVnode都有子节点
if (isDef(oldCh) && isDef(ch)) {
//当Vnode和oldvnode的子节点不同时,调用updatechilren函数,diff子节点
if (oldCh !== ch) updateChildren(elm,oldCh,ch,insertedVnodeQueue);
}
//如果vnode有子节点,oldvnode没子节点
else if (isDef(ch)) {
//oldvnode是text节点,则将elm的text清除
if (isDef(oldVnode.text)) api.setTextContent(elm,'');
//并添加vnode的children
addVnodes(elm,null,ch.length - 1,insertedVnodeQueue);
}
//如果oldvnode有children,而vnode没children,则移除elm的children
else if (isDef(oldCh)) {
removeVnodes(elm,oldCh.length - 1);
}
//如果vnode和oldvnode都没chidlren,且vnode没text,则删除oldvnode的text
else if (isDef(oldVnode.text)) {
api.setTextContent(elm,'');
}
}
//如果oldvnode的text和vnode的text不同,则更新为vnode的text
else if (oldVnode.text !== vnode.text) {
api.setTextContent(elm,vnode.text);
}
//patch完,触发postpatch钩子
if (isDef(hook) && isDef(i = hook.postpatch)) {
i(oldVnode,vnode);
}
}