从本质上来说,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 或者 reac t内部封装了diff 算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。
另外现代前端框架的一个基本要求就是无须手动操作 DOM,**一方面是因为手动操作 DOM 无法保证程序性能,多人协作的项目中如果 review 不严格,可能会有开发者写出性能较低的代码,**另一方面更重要的是省略手动 DOM 操作可以大大提高开发效率。
为什么要用 Virtual DOM:
(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能
下面对比一下修改DOM时真实 DOM 操作和 Virtual DOM 的过程,来看一下它们重排重绘的性能消耗∶
Virtual DOM 更新 DOM 的准备工作耗费更多的时间,也就是 JS 层面,相比于更多的 DOM 操作它的消费是极其便宜的。
尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,我依然可以给你提供过得去的性能。
(2)跨平台
Virtual DOM 本质上是 JavaScript 的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp 等。
实际上,diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,**将更新补丁作用于真实 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>;
}
}
这里,首先假定 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 节点,降低内耗。
Key 是 React 用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识。在开发过程中,我们需要 保证某个元素的 key 在其同级元素中具有唯一性
`。
在 React Diff 算法中 React 会借助元素的 Key 值来 判断该元素是新近创建的还是被移动而来的元素
,从而减少不必要的元素重渲染此外,React 还需要借助 Key 值来判断元素与本地状态的关联关系。
注意事项:
在用 v-for
更新已渲染的元素列表的时候,会使用就地复用的策略;
也就是说列表数据修改的时候,他会根据key值去判断某个值是否改变了,如果改变了就重新渲染,不然就复用之前的元素
。v-for
可以使用数据本身所具有的唯一值作为 key,也可以使用索引 index 作为 key
eg. 当我们修改 list,向其中插入一条数据时:
可见,除了 name 为 aa 的那条数据的 key 值没变外,另外两个都变了,也就是说 aa 可以复用, 而另外两条数据虽然值没变,但 key 值改变了,需要重新渲染,这种效率是很低的。这时我们可以使用 list 的 id 作为 key 来提高效率。
虚拟 DOM 相对原生的 DOM 不一定是效率更高,如果只修改一个按钮的文案,那么虚拟 DOM 的操作无论如何都不可能比真实的 DOM 操作更快。在首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,虚拟 DOM 也会比innerHTML 插入慢。它能保证性能下限,在真实 DOM 操作的时候进行针对性的优化时,还是更快的。所以要根据具体的场景进行探讨。
在整个 DOM 操作的演化过程中,其实主要矛盾并不在于性能,而在于开发者写得爽不爽,在于研发体验/研发效率。虚拟 DOM 不是别的,正是前端开发们为了追求更好的研发体验和研发效率而创造出来的高阶产物。虚拟 DOM 并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式(也就是函数式的 UI 编程方式)的同时,仍然保持一个还不错的性能。
diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁
。
React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。 但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型的比对,分别是树、组件及元素,以此提升效率。
以上是经典的 React diff 算法内容。
自 React 16
起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复
,节点与树分别采用了 FiberNode
与 FiberTree
进行重构。fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点。
整个更新过程由 current
与 workInProgress
两株树双缓冲完成。workInProgress 更新完成后,再通过修改 current 相关指针指向新节点。
Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力, 但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用 防抖和节流
去提高响应性能。