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.createElement 和 document.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?
首先要知道,React 与 Vue 原理是不同的,它们也是现代前端框架侦测数据的两大代表。
现代前端框架有两种方式侦测变化:pull、push
-
pull
主动发出动作才会触发DOM的更新,通过Diff查找变化的位置。
以React为代表,通过setState API、props显式更新数据。React会进行一层层的Virtual DOM Diff操作查找差异,然后Patch到DOM上。即React从一开始就不知道到底发生了变化,只知道变化了,只能通过比较暴力的Diff操作查找变化的具体位置。
React Fiber实现了分片管理,把更新过程碎片化。 -
另一个
pull的代表是Angular,脏值检测操作 -
push
Vue的响应式系统是push的代表,Vue程序在初始化时会对数据做依赖收集,每个指令({{xxx}}、v-text、v-model...)绑定一个Watcher,一旦数据变化,响应式系统会立刻感知,并修改相应DOM。即Vue从一开始就明确知道哪个位置的DOM发生了变化,完全不需要VNode和Diff!这也就是Vue1.x!但是,这种侦测与绑定方式 会因为细粒度过高而产生大量的
Watcher,带来的内存和依赖追踪的开销也很惊人;而细粒度过低又无法精确侦测变化。因此,Vue2.0的设计是选择中等细粒度的方案,并引入VNode与Diff,在组件级别进行push侦测。一旦侦测到变化的组件,就会在组件内部进行Virtual DOM Diff获取更加具体的差异。而Virtual DOM Diff其实是pull操作,所以Vue2.0是push + pull结合的方式进行变化侦测的。
另外,手动使用 watcher{ } 和 $watcher() 时,还会额外创建新的Watcher
很多时候手工优化DOM 确实会比
Virtual DOM效率高,对于比较简单的DOM结构用手工优化没有问题。但当页面结构很庞大,结构很复杂时,手工优化会花去大量时间,而且可维护性也不高,不能保证每个人都有手工优化的能力。至此,Virtual DOM的解决方案应运而生, 虽然它很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达平衡。
Virtual DOM 另一个重大意义就是提供一个中间层,为跨平台提供了可能性,JS去写UI,IOS、安卓之类的负责渲染,就像ReactNative一样。
Vue2.0加入了Virtual Dom,Vue的Diff位于patch.js文件中,该算法来源于snabbdom,复杂度为O(n)
举个例子
React代码会经过 @babel/preset-react(babel7) 编译到生成 Virtual DOM
生成虚拟DOM.png
-
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
- 用JS对象模拟
-
生成
VirtualDOM-1 --> createElement
通过图中Babel编译生成的代码可以看出,最终是通过createElement()去构建每一个节点的:- 通过构造函数
Element构造虚拟DOM节点
Element.png
- 通过
createElement()来生成Element构造函数的实例对象,即VirtualDOM-1
createElement.png
- 通过构造函数
-
将
VirtualDOM-1转化为真实DOM-> render
render.png
- 根据
VirtualDOM对象中的type属性,使用document.createElement()创建对应元素A - 遍历
VirtualDOM对象中的props,使用setAttr()把其中的属性-值设置到元素A上 - 遍历
VirtualDOM对象中的children,判断子节点是否继承Element构造函数,如果是,则递归,否则创建文本节点,并使用appendChild()添加到元素A的子节点上 - 至此,成功通过递归
VirtualDOM-1创建出对应的真实DOM!
- 根据
-
将真实DOM挂载到指定的根节点上
-> renderDOM
renderDOM.png
-
DOM-Diff
React的Diff其实和Vue的Diff大同小异,比对只会在同层级进行,不会跨层级比较
diff.png
由于用户操作导致生成了
VirtualDOM-2,比对VirtualDOM-1和VirtualDOM-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']
}
- 打补丁:将补丁对象应用到真实DOM上
通过VirtualDOM-1生成真实DOM,所以VirtualDOM-1和真实DOM的结构是一一对应的,然后又因为补丁对象是通过对VirtualDOM-1进行深度优先遍历生成的,那么只要对真实DOM进行深度优先遍历,那补丁对象中记录的索引(表示节点位置)就能和真实DOM对应上了,从而取出对应需要执行的DOM操作
打补丁.png
至此就完成了DOM的更新操作











网友评论