美文网首页
初探VirtualDOM与Diff

初探VirtualDOM与Diff

作者: hellomyshadow | 来源:发表于2020-04-03 06:50 被阅读0次

    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

    • pull
      主动发出动作才会触发DOM的更新,通过Diff查找变化的位置。
      React为代表,通过 setState API、props 显式更新数据。React会进行一层层的 Virtual DOM Diff 操作查找差异,然后 PatchDOM上。即 React从一开始就不知道到底发生了变化,只知道变化了,只能通过比较暴力的Diff操作查找变化的具体位置。
      React Fiber 实现了分片管理,把更新过程碎片化。

    • 另一个 pull 的代表是 Angular,脏值检测操作

    • push
      Vue的响应式系统是 push 的代表,Vue程序在初始化时会对数据做依赖收集,每个指令({{xxx}}、v-text、v-model...)绑定一个Watcher,一旦数据变化,响应式系统会立刻感知,并修改相应DOM。即 Vue从一开始就明确知道哪个位置的DOM发生了变化,完全不需要VNodeDiff!这也就是 Vue1.x !

      但是,这种侦测与绑定方式 会因为细粒度过高而产生大量的Watcher,带来的内存和依赖追踪的开销也很惊人;而细粒度过低又无法精确侦测变化。因此,Vue2.0的设计是选择中等细粒度的方案,并引入VNodeDiff,在组件级别进行 push 侦测。一旦侦测到变化的组件,就会在组件内部进行 Virtual DOM Diff 获取更加具体的差异。而 Virtual DOM Diff 其实是 pull 操作,所以Vue2.0push + pull 结合的方式进行变化侦测的。

    另外,手动使用 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 递增),如果发现有差异,则把索引值对应的所需操作存起来。

    比对规则

    • 节点类型不同,则执行节点替换模式 { type: REPLACE, newNode }
    • 节点类型相同,则比较属性,如果不相同,则执行 { type: ATTR, attrs: { class: 'list-group' } }
    • 新的节点不存在,则移除节点 { type: REMOVE }
    • 节点都是字符串且不相等,则是文本的变化 { type: TEXT, text: 'xxxx' }
      先序深度优先遍历
    先序深度优先遍历.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的更新操作

    相关文章

      网友评论

          本文标题:初探VirtualDOM与Diff

          本文链接:https://www.haomeiwen.com/subject/crlcphtx.html