虚拟DOM
# 对虚拟 DOM
的理解?虚拟 DOM
主要做了什么?虚拟 DOM
本身是什么?
从本质上来说,Virtual Dom
是一个JavaScript
对象,通过对象的方式来表示DOM
结构。将页面的状态抽象为JS
对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。
通过事务处理机制,将多次DOM
修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM
的重绘重排次数,提高渲染性能。
虚拟DOM
是对DOM
的抽象,这个对象是更加轻量级的对DOM
的描述。它设计的最初目的,就是更好的跨平台,比如node.js
就没有DOM
,如果想实现SSR
,那么一个方式就是借助虚拟dom
,因为虚拟dom
本身是js
对象。 在代码渲染到页面之前,vue
或者react
会把代码转换成一个对象(虚拟DOM
)。以对象的形式来描述真实dom
结构,最终渲染到页面。在每次数据发生变化前,虚拟dom
都会缓存一份,变化之时,现在的虚拟dom
会与缓存的虚拟dom
进行比较。在vue
或者react
内部封装了diff
算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
另外现代前端框架的一个基本要求就是无须手动操作DOM
,一方面是因为手动操作DOM
无法保证程序性能,多人协作的项目中如果review
不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动DOM
操作可以大大提高开发效率。
为什么要用 Virtual DOM
:
1. 保证性能下限,在不进行手动优化的情况下,提供过得去的性能
下面对比一下修改DOM
时真实DOM
操作和Virtual DOM
的过程,来看一下它们重排重绘的性能消耗∶
- 真实
DOM
∶ 生成HTML
字符串+ 重建所有的DOM
元素 Virtual DOM
∶ 生成vNode
+DOMDiff
+必要的DOM
更新
Virtual DOM
的更新DOM
的准备工作耗费更多的时间,也就是JS
层面,相比于更多的DOM
操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。
2. 跨平台 Virtual DOM
本质上是JavaScript
的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp
等。
# React diff
算法的原理是什么?
实际上,diff
算法探讨的就是虚拟 DOM
树发生变化后,生成 DOM
树更新补丁的方式。它通过对比新旧两株虚拟 DOM
树的变更差异,将更新补丁作用于真实 DOM
,以最小成本完成视图更新。 具体的流程如下:
- 真实的
DOM
首先会映射为虚拟DOM
; - 当虚拟
DOM
发生变化后,就会根据差距计算生成patch
,这个patch
是一个结构化的数据,内容包含了增加、更新、移除等; - 根据
patch
去更新真实的DOM
,反馈到用户的界面上。
一个简单的例子:
import React from 'react'
export default class ExampleComponent extends React.Component {
render() {
if(this.props.isVisible) {
return <div className="visible">visbile</div>;
}
return <div className="hidden">hidden</div>;
}
}
2
3
4
5
6
7
8
9
这里,首先假定 ExampleComponent
可见,然后再改变它的状态,让它不可见 。映射为真实的 DOM
操作是这样的,React
会创建一个 div
节点。
<div class="visible">visbile</div>
当把 visbile
的值变为 false
时,就会替换 class
属性为 hidden
,并重写内部的 innerText
为 hidden
。这样一个生成补丁、更新差异的过程统称为 diff
算法。
diff
算法可以总结为三个策略,分别从树、组件及元素三个层面进行复杂度的优化:
策略一:忽略节点跨层级操作场景,提升比对效率。(基于树进行对比)
这一策略需要进行树比对,即对树进行分层比较。树比对的处理手法是非常“暴力”的,即两棵树只对同一层次的节点进行比较,如果发现节点已经不存在了,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,这就提升了比对效率。
策略二:如果组件的 class
一致,则默认为相似的树结构,否则默认为不同的树结构。(基于组件进行对比)
在组件比对的过程中:
- 如果组件是同一类型则进行树比对;
- 如果不是则直接放入补丁中。
只要父组件类型不同,就会被重新渲染。这也就是为什么 shouldComponentUpdate
、PureComponent
及 React.memo
可以提高性能的原因。
策略三:同一层级的子节点,可以通过标记 key
的方式进行列表对比。(基于节点进行对比)
元素比对主要发生在同层级中,通过标记节点操作生成补丁。节点操作包含了插入、移动、删除等。其中节点重新排序同时涉及插入、移动、删除三个操作,所以效率消耗最大,此时策略三起到了至关重要的作用。通过标记 key
的方式,React
可以直接移动 DOM
节点,降低内耗。
# React key
是干嘛用的 为什么要加?key
主要是解决哪一类问题的
Keys
是 React
用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要保证某个元素的 key
在其同级元素中具有唯一性。
在 React Diff
算法中 React
会借助元素的 Key
值来判断该元素是新近创建的还是被移动而来的元素,从而减少不必要的元素重渲染此外,React
还需要借助 Key
值来判断元素与本地状态的关联关系。
注意事项:
key
值一定要和具体的元素—一对应;- 尽量不要用数组的
index
去作为key
; - 不要在
render
的时候用随机数或者其他操作给元素加上不稳定的key
,这样造成的性能开销比不加key
的情况下更糟糕。
# 虚拟 DOM
的引入与直接操作原生 DOM
相比,哪一个效率更高,为什么
虚拟DOM
相对原生的DOM
不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM
的操作无论如何都不可能比真实的 DOM
操作更快。在首次渲染大量 DOM
时,由于多了一层虚拟 DOM
的计算,虚拟DOM
也会比innerHTML
插入慢。它能保证性能下限,在真实DOM
操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。
在整个 DOM
操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM
不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM
并不一定会带来更好的性能,React
官方也从来没有把虚拟 DOM
作为性能层面的卖点对外输出过。
虚拟 DOM
的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI
编程方式)的同时,仍然保持一个还不错的性能。
# React
与 Vue
的 diff
算法有何不同?
diff
算法是指生成更新补丁的方式,主要应用于虚拟 DOM
树变化后,更新真实 DOM
。所以 diff
算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。
React
的 diff
算法,触发更新的时机主要在 state
变化与 hooks
调用之后。此时触发虚拟 DOM
树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3
种类型节点的比对,分别是树、组件及元素,以此提升效率。
- 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟
DOM
树只对同一层次的节点进行比较。 - 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。
- 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的
DOM
剪裁操作。
以上是经典的 React diff
算法内容。自 React 16
起,引入了 Fiber
架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode
与 FiberTree
进行重构。fiberNode
使用了双链表的结构,可以直接找到兄弟节点与子节点。整个更新过程由 current
与 workInProgress
两株树双缓冲完成。workInProgress
更新完成后,再通过修改 current
相关指针指向新节点。
Vue
的整体 diff
策略与 React
对齐,虽然缺乏时间切片能力,但这并不意味着 Vue
的性能更差,因为在 Vue 3
初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue
中其他的场景几乎都可以使用防抖和节流去提高响应性能。