ReactNative启动完成之后,就会加载jsbundle中的js代码,进入js层渲染。
为什么讲 ReactNative JS 层渲染,重点讲 diff 算法呢?

使用 React 写过 WebReactNative的,能很明显感觉到:除了组件命名不一样之外,生命周期、刷新机制等几乎是完全一样的,这也就是 Facebook 所说的“learn once, write anywhere”,只要会写 React,就能无压力同时开发 WebReactNative。而 React 框架相对于传统的纯 js 开发所具有的优势,核心就是组件化diff 算法刷新机制,这两点极大的提升了开发效率和程序的渲染性能。

React 通过setState界面刷新时,并不会马上对所有真实的 DOM 节点进行操作,而是先通过 diff 算法计算。然后,再对有变化的 DOM 节点进行操作(native 是对原生 UI 层进行操作),具体刷新步骤如下:

  1. state 变化,生成新的 Virtual Dom
  2. 比较 Virtual Dom 与之前 Virtual Dom 的异同;
  3. 生成差异对象;
  4. 遍历差异对象并更新真实 DOM;

Virtual Dom 概述

DOM 操作很耗时,使用 JS 对象来模拟 DOM Tree,在渲染更新时,先对 JS 对象进行操作,再批量将 JS 对象 Virtual Dom 渲染成 DOM Tree,从而减少对 DOM 的操作,提升性能。

整个 diff 算法,都是基于 Virtual Dom 的,那什么是 Virtual Dom 呢?

Virtual Dom 本质是用来模拟 DOMJS 对象。一般含有标签名(tag)、属性(props)和子元素对象(children)三个属性,不同框架对属性的命名会有点差别,但表达的意思是一致的。

例如:对于以下一段代码,怎么映射成对应的 Virtual Dom 呢?

<A>
  Hello World
  <B>
    <C key="key-C" style={{ width: 100 }} />
    <D key="key-D" style={{ color: 'red' }} />
  </B>
</A>
在该例中,按三个属性分析如下:
  • A、B、C、D是标签(tag
  • keystyle是属性(props
  • 节点B是节点A的子元素对象(children
  • 节点C和D是节点B的子元素对象(children
    最后,映射出来的 js 对象(Virtual Dom)如下:
    在这里插入图片描述

React Diff 算法的两个假设

为什么 React Diff 算法相对于传统的 diff 算法,复杂度从 O(n^3)降到 O(n)
React基于以下的两个假设,减少了不必要的计算。

1.两个相同组件将会生成相似的DOM结构,两个不同组件将会生成不同的DOM结构。
2.对于同一层次的一组子节点,它们可以通过唯一的id进行区分。

对于假设 1: 两个相同组件,一般指的是相同的类,包含 React 官方定义的组件(ViewText)和程序员自定义的组件(这也是React 组件化开发的一个原因,可以提升 diff 算法的效率);
对于假设 2: 一般指的是使用map遍历生成的列表视图或者使用ListView/FlatList等列表组件;

React Diff 算法的实现,几乎都是基于以上两个假设进行优化的,接下来来看看 React Diff 算法的具体实现原理。

React Diff 算法的实现

基于以上的两个假设,React Diff 算法的实现,主要可以分相同类型节点的比较、不同节点类型的比较、列表节点的比较三类情况,这里也主要针对这三类情况进行分析。

1. 相同类型节点的比较

修改节点 A 的属性 style
在这里插入图片描述

依据假设 1 的前半句 两个相同组件将会生成相似的 DOM 结构,由于新旧节点类型相同(tag 都是 A),DOM 结构没有发生变化,React 仅对属性(style)进行重设(将 styleBefore 改成将 styleAfter)从而实现节点的转换和界面的更新

这种情况,通过这类diff算法计算后,会调用 Native的 updateView 来刷新界面。

  1. 不同节点类型的比较

将节点 A 及其子节点(红色标签部分)改成节点 D 的子节点

为了方便分析,首先抽象成 DOM tree 节点模型,如下

依据假设假设 1 的后半句两个不同组件将会生成不同的 DOM 结构,当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较,因此,React 会直接删除前面的节点,然后创建并插入新的节点。

在该例子中,会直接先移除根结点的左子树(即先移除节点 A 及其子节点 B 和 C),然后再重新创建节点 A 及其子节点 B 和 C 作为根结点的右子树

这种情况,通过这类diff算法计算后,会调用 Native的 manageChildren 来刷新界面。

  1. 列表节点的比较
    在渲染列表节点时,它们一般都有相同的结构,只是内容有些不同而已,常见的,如使用map遍历生成的列表视图或者ListView/FlatList等列表组件,如果开发的时候没有写 key,编译器会给出警告提示(因为是否添加 key,对应的 diff 算法差别很大,程序性能也会差别很大)

依据假设 2对于同一层次的一组子节点,它们可以通过唯一的 key 进行区分,通过给每个节点添加唯一的 key,可以极大的简化 diff 算法,减少对 DOM 的操作。列表节点的比较主要有添加节点、删除节点、节点排序三种场景,js层diff算法计算后,会调用 Native的 manageChildren 来刷新界面。

3.1 添加节点

在节点 B 与 C 之间插入节点 F

每个节点是否添加唯一标识 key 的算法实现与对比

在方案一中,没有添加唯一的稳定的 key,无法定位到具体修改的位置,只能依次比较前后两个状态(即 A、B、C、D、E 和 A、B、F、C、D、E),当比较到第三个时,发现 C 与 F 不相同,记录下来,往后依次比较,
D 与 C,E 与 D,均不相同,也记录下来,最后加上新状态新增的一个 E 节点,一共需要对 DOM 进行 4 次操作

在方案二中,如果添加了唯一的稳定的 key,则可以直接找到插入的位置,对 DOM 进行 1 次插入操作即可

综上,可以发现,列表添加节点时,有唯一的稳定的 key,可以减少对 DOM 的操作,从而提升程序性能

3.2 删除节点

删除节点 B

在方案一中,没有添加唯一的稳定的 key,无法定位到具体修改的位置,只能依次比较前后两个状态(即 A、B、C 和 A、C),当比较到第二个时,发现 B 与 C 不相同,记录下来,往后依次比较,
发现新状态比旧状态少了节点 C,移除旧状态的节点 C,一共需要对 DOM 进行 2 次操作

在方案二中,如果添加了唯一的稳定的 key,则可以直接找到删除的位置,对 DOM 进行 1 次删除操作即可

综上,可以发现,列表删除节点时,有唯一的稳定的 key,可以减少对 DOM 的操作,从而提升程序性能

3.2 排序节点

交换节点 B 和 C 的位置

每个节点是否添加唯一标识 key 的算法实现与对比

在方案一中,在没有添加 key 的情况下,无法定位到具体的节点,只能通过遍历,依次比较,再逐个更新。比如在该例中,要交换 旧dom 和 新dom 的节点 B 和 C 的位置,执行操作如下:

1.新dom 的节点 C 与 旧dom 的节点 B 进行比较,节点 B 更新成节点 C

2.新dom 的节点 B 与 旧dom 的节点 C 进行比较,节点 C 更新成节点 B。
复制代码

在方案二中,在有唯一稳定的 key 的情况下,可以直接定位到具体的节点,只需要对相应的节点进行排序即可。比如在该例中,要交换 B 和 C 的位置,执行操作如下:

1.只需要交换节点 B、C 的位置即可。
复制代码

综上,可以发现没有 key 的情况下,需要对 DOM 进行 2 次操作,有 key 的情况下,只需要对 DOM 进行 1 次操作即可。

总结:列表节点中,有唯一稳定的 key 的情况下,无论是添加节点、删除节点、排序节点,都能极大的减少对 DOM 的操作次数,提升程序的性能

最后,为什么说 key 必须是唯一的,且是稳定的呢?

key如果不唯一(如不同节点的key取值相同),如果列表的所有组件类型相同,则与不设置key的diff算法也是一样的;但是,如果列表的组件类型不同,则可能会出现重复渲染等异常情况

如果key不稳定(如插入或删除节点时,key取index),则与不设置key的diff算法是一样的