vue的Virtual Dom实现snabbdom解密
vue在官方文档中提到与react的渲染性能对比中,因为其使用了snabbdom而有更优异的性能。 JavaScript 开销直接与求算必要 DOM 操作的机制相关。尽管 Vue 和 React 都使用了 Virtual Dom 实现这一点,但 Vue 的 Virtual Dom 实现(复刻自 snabbdom)是更加轻量化的,因此也就比 React 的实现更高效。 看到火到不行的国产前端框架vue也在用别人的 Virtual Dom开源方案,是不是很好奇snabbdom有何强大之处呢?不过正式解密snabbdom之前,先简单介绍下Virtual Dom。 什么是Virtual DomVirtual 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接口并没有绑定关系。
![]() ![]() DOM 通常被视为一棵树,元素则是这棵树上的节点(node),而 Virtual DOM 的基础,就是 Virtual Node 了。 Snabbdom 的 Virtual Node 则是纯数据对象,通过 vnode 模块来创建,对象属性包括:
可以看到 Virtual Node 用于创建真实节点的数据包括:
源码: snabbdom并没有直接暴露vnode对象给我们用,而是使用h包装器,h的主要功能是处理参数: vnode
从snabbdom的typescript的源码可以看出,其实就是这几种函数重载: patch 创建vnode后,接下来就是调用patch方法将Virtual Dom渲染成真实DOM了。patch是snabbdom的init函数返回的。
snabbdom.init传入modules数组,module用来扩展snabbdom创建复杂dom的能力。 不多说了直接上patch的源码: if (parent !== null) { 先判断新旧虚拟dom是否是相同层级vnode,是才执行patchVnode,否则创建新dom删除旧dom,判断是否相同vnode比较简单: patch方法里面实现了snabbdom 作为一个高效virtual dom库的法宝—高效的diff算法,可以用一张图示意: diff算法的核心是比较只会在同层级进行,不会跨层级比较。而不是逐层逐层搜索遍历的方式,时间复杂度将会达到 O(n^3)的级别,代价非常高,而只比较同层级的方式时间复杂度可以降低到O(n)。 patchVnode函数的主要作用是以打补丁的方式去更新dom树。 <div class="jb51code"> //如果oldvnode的text和vnode的text不同,则更新为vnode的text patchVnode将新旧虚拟DOM分为几种情况,执行替换textContent还是updateChildren。 updateChildren是实现diff算法的主要地方: 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操作的情况: 1.当oldStartVnode,newEndVnode相同层级时,说明oldStartVnode.el跑到oldEndVnode.el的后边了。 2.当oldEndVnode,newStartVnode相同层级时,说明oldEndVnode.el跑到了newStartVnode.el的前边。 3.newCh中的节点oldCh里没有,将新节点插入到oldStartVnode.el的前边。 在结束时,分为两种情况: 1.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将被插入到子节点的末尾。 2.newStartIdx > newEndIdx,可以认为newCh先遍历完。此时oldStartIdx和oldEndIdx之间的vnode在新的子节点里已经不存在了,调用removeVnodes将它们从dom里删除。 hook shabbdom主要流程的代码在上面就介绍完毕了,在上面的代码中可能看不出来如果要创建比较复杂的dom,比如有attribute、props、eventlistener的dom怎么办?奥秘就在与shabbdom在各个主要的环节提供了钩子。钩子方法中可以执行扩展模块,attribute、props、eventlistener等可以通过扩展模块实现。 在源码中可以看到hook是在snabbdom初始化的时候注册的。 snabbdom在全局下有6种类型的钩子,触发这些钩子时,会调用对应的函数对节点的状态进行更改首先我们来看看有哪些钩子以及它们触发的时间: 比如在patch的代码中可以看到调用了pre钩子 我们找一个比较简单的class模块来看下其源码: },{}]},{},[1])(1)
}); 可以看出create和update钩子方法调用的时候,可以执行class模块的updateClass:从elm中删除vnode中不存在的或者值为false的类。 将vnode中新的class添加到elm上去。 总结snabbdom
参考: 以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程之家。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |