操作真实的DOM耗时(JS引擎和渲染引擎互斥,上下文切换耗时;可能导致重排和重绘),但是引入虚拟 DOM 也并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式的同时,仍然保持一个还不错的性能。引用尤大大的一句话:“框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。”
如果存在跨层级的操作,React则直接判断移出子树那一层的组件消失了,对应子树需要被销毁;而移入子树的那一层新增了一个组件,需要重新为其创建一棵子树。销毁 + 重建的代价是昂贵的,因此React官方也建议开发者不要做跨层级的操作,尽量保持 DOM 结构的稳定性。
②、只有类型相同的元素才有Diff的必要
基于“若两个组件属于同一个类型,那么它们将拥有相同的DOM树形结构”这一规律,React认为,只有同类型的组件,才有进一步对比的必要性;若参与Diff的两个组件类型不同,那么直接放弃比较,原地替换掉旧的节点,如下图所示。只有确认组件类型相同后,React才会在保留组件对应 DOM 树(或子树)的基础上,尝试向更深层次去Diff。
什么是虚拟
DOM
将上面的
ele
打印出来,如下所示: 这就是虚拟
DOM
在React
中的形态,是JS
和DOM
之间的一个映射缓存,它在形态上表现为一个能够描述 DOM 结构及其属性信息的 JS 对象。为什么需要虚拟
DOM
提升开发者的体验和效率
引用
React
官网中的一段话:"Virtual DOM
赋予了React
声明式的API
:您告诉React
希望让UI
是什么状态,React
就确保DOM
匹配该状态。这使您可以从属性操作、事件处理和手动DOM
更新这些在构建应用程序时必要的操作中解放出来。" 从原生
JS
到JQuery
,开发者除了关心数据之外,还需要关心DOM
的操作;接着出现了模板引擎,虽然实现了用户界面与业务数据的分离,但是它涉及到大量的字符串的拼接,开发效率也比较低下。为了解决开发体验和开发效率与DOM
操作之间的矛盾,虚拟DOM
应运而生。解决跨平台不兼容的问题
虚拟
DOM
是对真实渲染内容的一层抽象,将视图层和渲染平台解耦了,它可以是对Web
页面真实DOM
的描述,也可以是对IOS
界面、安卓界面和小程序等的描述,实现"一套代码,多端运行"性能问题?
操作真实的
DOM
耗时(JS
引擎和渲染引擎互斥,上下文切换耗时;可能导致重排和重绘),但是引入虚拟 DOM 也并不一定会带来更好的性能,React 官方也从来没有把虚拟 DOM 作为性能层面的卖点对外输出过。虚拟 DOM 的优越之处在于,它能够在提供更爽、更高效的研发模式的同时,仍然保持一个还不错的性能。引用尤大大的一句话:“框架给你的保证是,你在不需要手动优化的情况下,我依然可以给你提供过得去的性能。” 性能是一个比较复杂复杂的问题,需要在一个特定的场景下谈论才有意义,举一个极端的例子,如果数据内容整个发生了改变,此时
DOM
更新的工作量是一致的,而虚拟DOM
却会产生开销更大的JS
计算,因此此时虚拟DOM
的优势是发挥不出来的。React
中的虚拟DOM
是怎么产生的 虚拟
DOM
的产生需要借助于Babel
可以看到
JSX
标签都被编译成了React.createElement
,也就是说JSX
本质其实是React.createElement
这个JS
的语法糖,nameReact.createElement
是怎样转化成虚拟DOM
的?
createElement
的源码如下: 从上面的源码可以看出来,
createElement
的主要逻辑就是格式化数据,将开发者传入的参数转换成符合ReactElement
方法的参数。ReactElement
的源码如下: 从源码可以看出,
ReactElelemt
的主要逻辑就是把入参分装进element
对象,然后将其返回,这个element
对象就是一开始我们打印的那个虚拟DOM
。 现在可以得出的结论是,
JSX
最终被转化成了一个element
对象即虚拟DOM
。diff
算法 将前后两次的虚拟
DOM
树进行对比,定位出具体需要更新的部分,生成一个“补丁集”,这就是diff
算法。diff
算法基于以下三个规律: ①、若两个组件属于同一个类型,那么它们将拥有相同的
DOM
树结构 ②、
DOM
节点之间的跨层级操作并不多,主要是同层级操作。 ③、处于同一层级的一组子节点,可用通过设置 key 作为唯一标识,从而维持各个节点在不同渲染过程中的稳定性
Diff
算法的核心要点主要是三个: ①、分层对比
基于“
DOM
节点之间的跨层级操作并不多,同层级操作是主流”这一规律,React
的Diff
过程直接放弃了跨层级的节点比较,它只针对相同层级的节点作对比。 如果存在跨层级的操作,
React
则直接判断移出子树那一层的组件消失了,对应子树需要被销毁;而移入子树的那一层新增了一个组件,需要重新为其创建一棵子树。销毁 + 重建的代价是昂贵的,因此React
官方也建议开发者不要做跨层级的操作,尽量保持DOM
结构的稳定性。 ②、只有类型相同的元素才有
Diff
的必要 基于“若两个组件属于同一个类型,那么它们将拥有相同的
DOM
树形结构”这一规律,React
认为,只有同类型的组件,才有进一步对比的必要性;若参与Diff
的两个组件类型不同,那么直接放弃比较,原地替换掉旧的节点,如下图所示。只有确认组件类型相同后,React
才会在保留组件对应DOM
树(或子树)的基础上,尝试向更深层次去Diff
。 ③、属性
key
可以提高节点的复用性
React
官方对key
的定义是:key
是用来帮助React
识别哪些内容被更改、添加或者删除。key
需要写在用数组渲染出来的元素内部,并且需要赋予其一个稳定的值。稳定在这里很重要,因为如果key
值发生了变更,React
则会触发UI
的重渲染。这是一个非常有用的特性。 所以,
key
想解决的是同一层级中节点的重用问题。如下图所示,如果想在组件A
的两个子节点B
和D
之间插入一个新的节点C
如果没加
key
,两棵树之间的Diff
过程应该是这样的: 首先对比位于第 1 层的节点,发现两棵树的节点类型是一致的(都是
A
),于是进一步Diff
; 开始对比位于第 2 层的节点,第 1 个接受比较的是
B
这个位置,对比下来发现两棵树这个位置上的节点都是B
,继续下个节点的diff
第 2 个接受比较的是
D
这个位置,对比D
和C
,发现前后的类型不一致,直接删掉D
重建C
; 第 3 个接受比较的是
E
这个位置,对比E
和D
,发现前后的类型不一致,直接删掉E
重建D
; 最后接受比较的是树 2 的
E
节点这个位置,这个位置在树 1 里是空的,也就是说树 2 的E
是一个新增节点,所以新增一个E
。 如果有
key
,如下图所示: 那么
key
就可以充当每个节点的 唯一标识,有了这个标识之后,当C
被插入到B
和D
之间时,React
会通过识别ID
,意识到D
和E
并没有发生变化,只是被调整了顺序而已。接着,React
便能够轻松地重用它“追踪”到旧的节点,将D
和E
转移到新的位置,并完成对C
的插入。这样一来,同层级下元素的操作成本便大大降低。