虚拟DOM和diff算法
虚拟DOM
背景:以前使用jq等命令式的完成一些DOM操作,伴随着前端工程化的不断发展,涌现了react、vue等MVVM框架,不用再关心具体的DOM操作,而是把重点放在了基于数据状态的操作,一旦数据更改,相应的DOM元素也会跟着变化,这种声明式的开发方式极大地提高了开发体验,更好地帮助我们实现组件复用、逻辑解耦等
虚拟DOM
本质上是JS对象,是真实DOM树的抽象
由于单次DOM API调用性能就不够好,频繁调用就会迅速积累损耗,但是我们又不可能不去操作DOM,并且很多DOM API的读写都涉及回流重绘,会更加地消耗性能,因此解决问题的本质是要减少不必要的DOM API调用,虚拟DOM将DOM的比对操作放在JS层,减少不必要的DOM API的调用,进而减少回流重绘,提高性能
虚拟DOM不一定比真实DOM更快,如果有大量数据改变时,虚拟DOM还存在diff算法的比对过程,上述性能优势仅仅适用于大量数据的渲染并且改变的数据只是其中一小部分的情况
虚拟DOM更加优秀的地方在于:
- 它打开了函数式的UI编程的大门,
UI=f(data)
这种方式构建UI,开发者不需要再去考虑具体的DOM操作,减少不必要的DOM API调用,可以以更加声明式的方式书写代码 - 虚拟DOM是以js对象为基础而不依赖具体的平台环境,支持跨平台开发,比如React Native、node等
react是如何通过一个js对象将虚拟DOM和真实DOM对应起来的呢?
-
虚拟DOM本质上是通过React.createElement函数创建的
-
craeteElement接收三个参数:
type
、config
、children
-
type:虚拟DOM的类型,可以是DOM元素类型,也可以是React组件类型
-
config:传入的元素上的属性组成的对象
-
children:是一个数组,代表该元素的子元素
-
最终会生成一个对象:
-
含有一个
$$typeof
属性值为REACT_ELEMENT_TYPE
,代表这是个React元素 -
key和ref从config中被单独抽取出来,放在ReactElement下
-
props包含了两部分:除去了key和ref的config、children数组,数组成员也是通过React.createElement生成的对象
-
_owner:就是Fiber
-
通过以上这些属性,react就可以用js对象把dom树上的结构信息、属性信息轻易表达出来了
-
直接通过React.createElement()创建虚拟DOM过于繁琐,JSX创建DOM的实现方式是React.createElement()的语法糖
Diff算法
设计动机
调用render()时会创建一棵React元素组成的树,在下一次state或props更新时,相同的render()会返回一棵不同的树,react需要找出两棵树的差异部分并且渲染到页面上
为了得到将一棵树转换成另一棵树的最少操作次数,最开始最优的算法也有O(n^3)的复杂度,于是react基于两个假设:
- 两个不同类型的元素会产生不同的树
- 开发者可以通过设置key属性,来判断哪些子元素在不同的渲染下可以保持不变
提出了O(n)的diff算法
为了降低复杂度,diff算法会预设三个限制:
- 只对同级元素进行diff,如果有一个DOM在其前后两次更新中跨越了层级,react不会尝试复用它
- 两个不同类型的元素会产生不同的树,如果div变成p,react会销毁div创建p
- 可以通过key值来暗示哪些子元素在不同的渲染下能保持稳定
Diff算法
diff算法的本质是对比render()返回的jsx对象newChild
和current Fiber
,生成workInProgress Fiber
从diff算法的入口函数reconcileChildFibers
出发,该函数会根据newChild
(jsx对象)类型调用不同的处理函数
根据同级节点数量将diff分为两类:
- 当
newChild
为object
、number
、string