初探VirtualDOM与Diff

2020-04-03  本文已影响0人  hellomyshadow

CSS的解析是从右往左逆向解析的(从DOM树的【下-上】解析比【上-下】解析效率高),嵌套标签越多,解析越慢

DOM Diff

Diff(比对)渲染更新前后产生的两个虚拟DOM对象,并产出差异补丁对象,再将差异补丁对象应用到真实DOM节点上。

操作DOM的代价是昂贵的,原生JS/jQuery操作DOM时,浏览器会从构建DOM树到绘制全部执行一遍。因为操作DOM的本质是 两个线程(JS引擎和GUI渲染引擎)间发送指令(通信) 的过程,并且浏览器在初始化一个元素时,会为其创建很多很多属性。所以在大量操作DOM的场景下,必然就会浪费大量性能。虚拟DOM的出现就是为了解决这个问题,通过一些计算来尽可能地减少操作DOM 保证了性能的下限。

当然,DOM Diff不一定更快!

Virtual DOM

一句话概括:用一个简单JavaScript的树形结构对象来描述复杂的真实DOM结构
一个标准的真实DOM元素会实现很多很多属性,而JavaScript对象中只存储对应真实DOM的一些重要参数,这样的JavaScript对象就是虚拟DOM --> Virtual DOM
Virtual DOM对应的是真实DOM, 使用document.createElementdocument.createTextNode创建的就是真实DOM节点,通过appendChild()/insertBefore()插入到真实DOM树中。

Virtual DOM就是利用JS运行速度快的特点对操作DOM进行优化的,用JS对象模拟DOM树(virtual node,VNode),在VNode中最小化处理DOM的变动,再应用到真实DOM上,提高渲染效率。

为什么不直接修改DOM,而是多加一层Virtual DOM,而且还要Diff

通过Vue底层原理手写源码可知,Vue可以通过数据劫持与Watcher精准探测到 每个具体DOM上绑定的数据变化,为什么还需要VNode(虚拟DOM)Diff

首先要知道,ReactVue 原理是不同的,它们也是现代前端框架侦测数据的两大代表。
现代前端框架有两种方式侦测变化:pull、push

另外,手动使用 watcher{ }$watcher() 时,还会额外创建新的Watcher

很多时候手工优化DOM 确实会比 Virtual DOM 效率高,对于比较简单的DOM结构用手工优化没有问题。但当页面结构很庞大,结构很复杂时,手工优化会花去大量时间,而且可维护性也不高,不能保证每个人都有手工优化的能力。至此, Virtual DOM的解决方案应运而生, 虽然它很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。

Virtual DOM 另一个重大意义就是提供一个中间层,为跨平台提供了可能性,JS去写UIIOS、安卓之类的负责渲染,就像ReactNative一样。

Vue2.0加入了Virtual Dom,Vue的Diff位于patch.js文件中,该算法来源于snabbdom,复杂度为O(n)

举个例子

React代码会经过 @babel/preset-react(babel7) 编译到生成 Virtual DOM

生成虚拟DOM.png
  1. Virtual DOM 从初次渲染到更新:
    初次渲染 -> 生成VirtualDOM-1对象 -> 递归VirtualDOM-1对象创建真实DOM并插入页面中 -> Diff前后产生的VirtualDOM得到差异对象 -> 把差异对象应用到真实DOM节点上

    • 用JS对象模拟DOM -> VirtualDOM-1
    • VirtualDOM-1转成真实DOM并插入页面中-> render
    • 如果有事件发生(用户操作更新数据)修改了VirtualDOM-1,则产生虚拟VirtualDOM-2,比较两棵 VirtualDOM 树,得到差异对象 -> Diff
    • 把差异对象应用到真实DOM树上-> patch
  2. 生成VirtualDOM-1 --> createElement
    通过图中Babel编译生成的代码可以看出,最终是通过createElement()去构建每一个节点的:

    • 通过构造函数 Element 构造虚拟DOM节点
      Element.png
    • 通过 createElement() 来生成 Element 构造函数的实例对象,即 VirtualDOM-1
      createElement.png
  3. VirtualDOM-1 转化为真实DOM -> render

    render.png
    • 根据 VirtualDOM 对象中的 type 属性,使用 document.createElement() 创建对应元素A
    • 遍历 VirtualDOM 对象中的 props,使用setAttr()把其中的属性-值设置到元素A
    • 遍历 VirtualDOM 对象中的 children,判断子节点是否继承Element构造函数,如果是,则递归,否则创建文本节点,并使用 appendChild() 添加到元素A的子节点上
    • 至此,成功通过递归VirtualDOM-1创建出对应的真实DOM!
  4. 将真实DOM挂载到指定的根节点上 -> renderDOM

    renderDOM.png
  5. DOM-Diff
    React的 Diff其实和Vue的 Diff 大同小异,比对只会在同层级进行,不会跨层级比较

    diff.png

由于用户操作导致生成了VirtualDOM-2,比对 VirtualDOM-1VirtualDOM-2的差异。
分析:通过深度优先遍历进行比对(只比较同级节点,不跨级比较),每次遍历到一个节点,就记录一个索引值(从 0 递增),如果发现有差异,则把索引值对应的所需操作存起来。

比对规则

先序深度优先遍历.png

由上图可以看出,标红 0,2,4,6 的节点发生了改变,所产出的 pathes 补丁对象是:

{
    0: [type: ATTRS, attrs: { class: 'list' }],
    2: [type: TEXT, text: 'd'],
    4: [type: TEXT, text: 'e'],
    6: [type: TEXT, text: 'f']
}
  1. 打补丁:将补丁对象应用到真实DOM上
    通过 VirtualDOM-1 生成真实DOM,所以 VirtualDOM-1 和真实DOM的结构是一一对应的,然后又因为补丁对象是通过对 VirtualDOM-1 进行深度优先遍历生成的,那么只要对真实DOM进行深度优先遍历,那补丁对象中记录的索引(表示节点位置)就能和真实DOM对应上了,从而取出对应需要执行的DOM操作
打补丁.png

至此就完成了DOM的更新操作

上一篇下一篇

猜你喜欢

热点阅读