vue在官方文档中提到与react的渲染性能对比中,由于其使用了snabbdom而有更优异的性能。javascript
JavaScript 开销直接与求算必要 DOM 操做的机制相关。尽管 Vue 和 React 都使用了 Virtual Dom 实现这一点,但 Vue 的 Virtual Dom 实现(复刻自 snabbdom)是更加轻量化的,所以也就比 React 的实现更高效。html
看到火到不行的国产前端框架vue也在用别人的 Virtual Dom开源方案,是否是很好奇snabbdom有何强大之处呢?不过正式解密snabbdom以前,先简单介绍下Virtual Dom。前端
Virtual Dom能够看作一棵模拟了DOM树的JavaScript树,其主要是经过vnode,实现一个无状态的组件,当组件状态发生更新时,而后触发Virtual Dom数据的变化,而后经过Virtual Dom和真实DOM的比对,再对真实DOM更新。能够简单认为Virtual Dom是真实DOM的缓存。vue
咱们知道,当咱们但愿实现一个具备复杂状态的界面时,若是咱们在每一个可能发生变化的组件上都绑定事件,绑定字段数据,那么很快因为状态太多,咱们须要维护的事件和字段将会愈来愈多,代码也会愈来愈复杂,因而,咱们想咱们可不能够将视图和状态分开来,只要视图发生变化,对应状态也发生变化,而后状态变化,咱们再重绘整个视图就行了。java
这样的想法虽好,可是代价过高了,因而咱们又想,能不能只更新状态发生变化的视图?因而Virtual Dom应运而生,状态变化先反馈到Virtual Dom上,Virtual Dom在找到最小更新视图,最后批量更新到真实DOM上,从而达到性能的提高。node
除此以外,从移植性上看,Virtual Dom还对真实dom作了一次抽象,这意味着Virtual Dom对应的能够不是浏览器的DOM,而是不一样设备的组件,极大的方便了多平台的使用。若是是要实现先后端同构直出方案,使用Virtual Dom的框架实现起来是比较简单的,由于在服务端的Virtual Dom跟浏览器DOM接口并无绑定关系。react
基于 Virtual DOM 的数据更新与UI同步机制:
git
初始渲染时,首先将数据渲染为 Virtual DOM,而后由 Virtual DOM 生成 DOM。github
数据更新时,渲染获得新的 Virtual DOM,与上一次获得的 Virtual DOM 进行 diff,获得全部须要在 DOM 上进行的变动,而后在 patch 过程当中应用到 DOM 上实现UI的同步更新。web
Virtual DOM 做为数据结构,须要能准确地转换为真实 DOM,而且方便进行对比。
介绍完Virtual DOM,咱们应该对snabbdom的功用有个认识了,下面具体解剖下snabbdom这只“小麻雀”。
DOM 一般被视为一棵树,元素则是这棵树上的节点(node),而 Virtual DOM 的基础,就是 Virtual Node 了。
Snabbdom 的 Virtual Node 则是纯数据对象,经过 vnode 模块来建立,对象属性包括:
能够看到 Virtual Node 用于建立真实节点的数据包括:
源码:
//VNode函数,用于将输入转化成VNode /** * * @param sel 选择器 * @param data 绑定的数据 * @param children 子节点数组 * @param text 当前text节点内容 * @param elm 对真实dom element的引用 * @returns {{sel: *, data: *, children: *, text: *, elm: *, key: undefined}} */ function vnode(sel, data, children, text, elm) { var key = data === undefined ? undefined : data.key; return { sel: sel, data: data, children: children, text: text, elm: elm, key: key }; }
snabbdom并无直接暴露vnode对象给咱们用,而是使用h包装器,h的主要功能是处理参数:
h(sel,[data],[children],[text]) => vnode
从snabbdom的typescript的源码能够看出,其实就是这几种函数重载:
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;
建立vnode后,接下来就是调用patch方法将Virtual Dom渲染成真实DOM了。patch是snabbdom的init函数返回的。
snabbdom.init传入modules数组,module用来扩展snabbdom建立复杂dom的能力。
很少说了直接上patch的源码:
return function patch(oldVnode, vnode) { var i, elm, parent; //记录被插入的vnode队列,用于批触发insert var insertedVnodeQueue = []; //调用全局pre钩子 for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); //若是oldvnode是dom节点,转化为oldvnode if (isUndef(oldVnode.sel)) { oldVnode = emptyNodeAt(oldVnode); } //若是oldvnode与vnode类似,进行更新 if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { //不然,将vnode插入,并将oldvnode从其父节点上直接删除 elm = oldVnode.elm; parent = api.parentNode(elm); createElm(vnode, insertedVnodeQueue); if (parent !== null) { api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 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; };
先判断新旧虚拟dom是不是相同层级vnode,是才执行patchVnode,不然建立新dom删除旧dom,判断是否相同vnode比较简单:
function sameVnode(vnode1, vnode2) { //判断key值和选择器 return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel; }
patch方法里面实现了snabbdom 做为一个高效virtual dom库的法宝—高效的diff算法,能够用一张图示意:
diff算法的核心是比较只会在同层级进行, 不会跨层级比较。而不是逐层逐层搜索遍历的方式,时间复杂度将会达到 O(n^3)的级别,代价很是高,而只比较同层级的方式时间复杂度能够下降到O(n)。
patchVnode函数的主要做用是以打补丁的方式去更新dom树。
function patchVnode(oldVnode, vnode, 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, elm, oldVnode.elm); removeVnodes(parentElm, [oldVnode], 0, 0); return; } //若是vnode和oldvnode类似,那么咱们要对oldvnode自己进行更新 if (isDef(vnode.data)) { //首先调用全局的update钩子,对vnode.elm自己属性进行更新 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode); //而后调用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, 0, ch.length - 1, insertedVnodeQueue); } //若是oldvnode有children,而vnode没children,则移除elm的children else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, 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); } }
patchVnode将新旧虚拟DOM分为几种状况,执行替换textContent仍是updateChildren。
updateChildren是实现diff算法的主要地方:
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) { var oldStartIdx = 0, newStartIdx = 0; var oldEndIdx = oldCh.length - 1; var oldStartVnode = oldCh[0]; var oldEndVnode = oldCh[oldEndIdx]; var newEndIdx = newCh.length - 1; var newStartVnode = newCh[0]; var newEndVnode = newCh[newEndIdx]; var oldKeyToIdx; var idxInOld; var elmToMove; var before; 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)) { patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm)); oldStartVnode = oldCh[++oldStartIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } idxInOld = oldKeyToIdx[newStartVnode.key]; if (isUndef(idxInOld)) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); newStartVnode = newCh[++newStartIdx]; } else { elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm); } else { patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined; api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm); } newStartVnode = newCh[++newStartIdx]; } } } if (oldStartIdx > oldEndIdx) { 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); } }
updateChildren的代码比较有难度,借助几张图比较好理解些:
过程能够归纳为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。若是4种比较都没匹配,若是设置了key,就会用key进行比较,在比较的过程当中,变量会往中间靠,一旦StartIdx>EndIdx代表oldCh和newCh至少有一个已经遍历完了,就会结束比较。
具体的diff分析:
对于与sameVnode(oldStartVnode, newStartVnode)和sameVnode(oldEndVnode,newEndVnode)为true的状况,不须要对dom进行移动。
有3种须要dom操做的状况:
当oldStartVnode,newEndVnode相同层级时,说明oldStartVnode.el跑到oldEndVnode.el的后边了。
当oldEndVnode,newStartVnode相同层级时,说明oldEndVnode.el跑到了newStartVnode.el的前边。
newCh中的节点oldCh里没有,将新节点插入到oldStartVnode.el的前边。
在结束时,分为两种状况:
oldStartIdx > oldEndIdx,能够认为oldCh先遍历完。固然也有可能newCh此时也正好完成了遍历,统一都归为此类。此时newStartIdx和newEndIdx之间的vnode是新增的,调用addVnodes,把他们所有插进before的后边,before不少时候是为null的。addVnodes调用的是insertBefore操做dom节点,咱们看看insertBefore的文档:parentElement.insertBefore(newElement, referenceElement)若是referenceElement为null则newElement将被插入到子节点的末尾。若是newElement已经在DOM树中,newElement首先会从DOM树中移除。因此before为null,newElement将被插入到子节点的末尾。
newStartIdx > newEndIdx,能够认为newCh先遍历完。此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除。
shabbdom主要流程的代码在上面就介绍完毕了,在上面的代码中可能看不出来若是要建立比较复杂的dom,好比有attribute、props
、eventlistener的dom怎么办?奥秘就在与shabbdom在各个主要的环节提供了钩子。钩子方法中能够执行扩展模块,attribute、props
、eventlistener等能够经过扩展模块实现。
在源码中能够看到hook是在snabbdom初始化的时候注册的。
var hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; var h_1 = require("./h"); exports.h = h_1.h; var thunk_1 = require("./thunk"); exports.thunk = thunk_1.thunk; function init(modules, domApi) { var i, j, cbs = {}; var api = domApi !== undefined ? domApi : htmldomapi_1.default; for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = []; for (j = 0; j < modules.length; ++j) { var hook = modules[j][hooks[i]]; if (hook !== undefined) { cbs[hooks[i]].push(hook); } } }
snabbdom在全局下有6种类型的钩子,触发这些钩子时,会调用对应的函数对节点的状态进行更改首先咱们来看看有哪些钩子以及它们触发的时间:
Name | Triggered when | Arguments to callback |
---|---|---|
pre |
the patch process begins | none |
init |
a vnode has been added | vnode |
create |
a DOM element has been created based on a vnode | emptyVnode, vnode |
insert |
an element has been inserted into the DOM | vnode |
prepatch |
an element is about to be patched | oldVnode, vnode |
update |
an element is being updated | oldVnode, vnode |
postpatch |
an element has been patched | oldVnode, vnode |
destroy |
an element is directly or indirectly being removed | vnode |
remove |
an element is directly being removed from the DOM | vnode, removeCallback |
post |
the patch process is done | none |
好比在patch的代码中能够看到调用了pre钩子
return function patch(oldVnode, vnode) { var i, elm, parent; var insertedVnodeQueue = []; for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i](); if (!isVnode(oldVnode)) { oldVnode = emptyNodeAt(oldVnode); }
咱们找一个比较简单的class模块来看下其源码:
function updateClass(oldVnode, vnode) { var cur, name, elm = vnode.elm, oldClass = oldVnode.data.class, klass = vnode.data.class; if (!oldClass && !klass) return; if (oldClass === klass) return; oldClass = oldClass || {}; klass = klass || {}; for (name in oldClass) { if (!klass[name]) { elm.classList.remove(name); } } for (name in klass) { cur = klass[name]; if (cur !== oldClass[name]) { elm.classList[cur ? 'add' : 'remove'](name); } } } exports.classModule = { create: updateClass, update: updateClass }; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.classModule; },{}]},{},[1])(1) });
能够看出create和update钩子方法调用的时候,能够执行class模块的updateClass:从elm中删除vnode中不存在的或者值为false的类
将vnode中新的class添加到elm上去
。
https://github.com/snabbdom/snabbdom/
https://segmentfault.com/a/1190000009017324
https://segmentfault.com/a/1190000009017349
http://web.jobbole.com/90831/