虚拟DOM是什么?
本质就是一个JS对象描述DOM节点,可以理解为将dom节点抽象成一个对象。
有什么用?
提供与真实DOM节点所对应的虚拟节点vnode
将虚拟节点vnode和旧虚拟节点oldVnode进行对比,然后更新视图
Virtual DOM优势
具备跨平台的优势
由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。
因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。
- 提升渲染性能
Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。
为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。我们通过patch 的核心—-diff 算法,找出本次DOM需要更新的节点来更新,其他的不更新。比如修改某个model 100次,从1加到100,那么有了Virtual DOM的缓存之后,只会把最后一次修改patch到view上。那diff 算法的实现过程是怎样的?
渲染过程

调用链关系:数据更新、渲染组件等—>调用render()生成vnode—>交给update()函数更新,_update()函数有__patch()方法,在里面进行虚拟Dom diff等更新实际dom。
- 渲染函数:渲染函数是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然我们也可以不写模板,直接写渲染函数,以获得更好的控制。
- VNode 虚拟节点:它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。
- patch(也叫做patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。
diff算法

Vue的diff算法是基于snabbdom改造过来的,仅在同级的vnode间做diff,递归地进行同级vnode的diff,最终实现整个DOM树的更新。因为跨层级的操作是非常少的,忽略不计,这样时间复杂度就从O(n3)变成O(n)。
diff 算法包括几个步骤:
- 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
- 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
- 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了
patch
判断两节点是否值得比较,值得比较则执行patchVnode
不值得比较则用Vnode替换oldVnode
1 | function patch (oldVnode, vnode, hydrating, removeOnly) { |
这个函数其实主要就是对children进行diff以决定该如何更新。
patchVnode
1 | function patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) { |
patchVnode的规则
- 如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),那么只需要替换elm以及componentInstance即可(原地复用)。
- 新老节点均有children子节点且不同,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。
- 如果只有新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。
- 如果只有老节点有子节点,直接删除老节点的子节点。
- 当新老节点都无子节点的时候,只是文本的替换
updateChildren
1 | function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue,removeOnly) { |
如果是oldS和newEndIdx匹配上了,那么真实dom中的第一个节点会移到最后
如果是oldE和newStartIdx匹配上了,那么真实dom中的最后一个节点会移到最前,匹配上的两个指针向中间移动
如果四种匹配没有一对是成功的,分为两种情况
- 如果新旧子节点都存在key,那么会根据oldChild的key生成一张hash表,用newStartIdx的key与hash表做匹配,匹配成功就判断newStartIdx和匹配节点是否为sameNode,如果是,就在真实dom中将成功的节点移到最前面,否则,将S生成对应的节点插入到dom中对应的oldS位置,newStartIdx指针向中间移动,被匹配old中的节点置为null。
- 如果没有key,则直接将newStartIdx生成新的节点插入真实DOM(ps:这下可以解释为什么v-for的时候需要设置key了,如果没有key那么就只会做四种匹配,就算指针中间有可复用的节点都不能被复用了)
虚拟dom更快吗
Virtual DOM render + diff 显然比渲染 html 字符串要慢,但是!它依然是纯 js 层面的计算,比起后面的 DOM 操作来说,依然便宜了太多。可以看到,innerHTML 的总计算量不管是 js 计算还是 DOM 操作都是和整个界面的大小相关,但 Virtual DOM 的计算量里面,只有 js 计算和界面大小相关,DOM 操作是和数据的变动量相关的。前面说了,和 DOM 操作比起来,js 计算是极其便宜的。
它保证了 1)不管你的数据变化多少,每次重绘的性能都可以接受;2) 你依然可以用类似 innerHTML 的思路去写你的应用。
为什么diff算法是O(n)?
正常的diff算法是O(n^3) 两颗树每个节点两两比对是O(n^2) ,在进行一次增删改就是O(n^3)
但Vue的diff算法是层级遍历,如果最外层节点不一样就认为没必要深入遍历了,如果一样然后在比对每一层级里面的节点
参考
https://juejin.im/post/6844903923183157261
https://blog.fundebug.com/2019/06/26/vue-virtual-dom/
https://juejin.im/post/6844903607913938951#heading-6