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

-
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' }
先序深度优先遍历

由上图可以看出,标红 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操作

至此就完成了DOM的更新操作
网友评论