每当 state 发生变化,ReactJS 就会重新渲染新的 Virtual DOM,接着执行 Diff 算法与旧的 Virtual DOM 比较,计算出差异,最后执行 DOM 操作。ReactJS 基于以下两个假设优化了标准实现的 Diff 算法:
- 相同的组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构
- 每个元素都有可能由唯一的 key 所标识
这两个假设作为高性能 Diff 算法与渲染机制的基础,不仅能够指导我们编写高性能的 render 函数,也有助于我们进一步优化渲染,从而提升界面的构建性能。
相同的组件产生类似的 DOM 结构,不同的组件产生不同的 DOM 结构
基于第一个假设,主要影响了不同结点的之间比较。这种比较又分成两种情况:结点类型不同和结点属性不同。
对于不同类型的结点 ,ReactJS 先删除旧结点而后插入新结点,对于不同组件,操作规则同样适用,因为不同组件产生不同 DOM 结构。当对比相同类型结点时,例如遇到新、旧 Virtual DOM 同一位置的结点相同时,ReactJS 会进一步比较 不同的 DOM 结点属性 ,对于不同的属性结点会执行相应的新增、替换、删除等操作(style 属性之所以为对象而非字符串是为了计算差异而后执行所需的操作)。
不过 ReactJS 的 Diff 算法实现非常简单,只进行同层次的比较,也就是同一父结点下的所有子节点的比较。例如把 A 结点的子节点 C 移动到 B 结点,然后生成以下两棵 Virtual DOM 树,ReactJS 分析出差异之后 A --> null, B --> C
,从 A 结点把 C 结点删除,然后创建新的 C 结点并把 C 结点添加到 B 结点,而非简单地把 C 结点移动到 B 结点。
+---+ +---+ destroy C
| R | | R | create C
+-+-+ +-+-+ append C to B
| |
+---+---+ +---+---+
| | | | [lifecycle]
+-+-+ +-+-+ +-+-+ +-+-+
| A | | B | | A | | B | C will unmount (componentWillUnmout)
+-+-+ +---+ +---+ +-+-+ C is created (constructor)
| | A is updated (componentDidUpdate)
+-+-+ +-+-+ C did mount (componentDidMount)
| C | | C | B is updated (componentDidUpdate)
+---+ +---+ R is updated (componentDidUpdate)
+---+ +---+ destroy C
| R | | R | create C
+-+-+ +-+-+ append C to B
| |
+---+---+ +---+---+
| | | | [lifecycle]
+-+-+ +-+-+ +-+-+ +-+-+
| A | | B | | A | | B | C will unmount (componentWillUnmout)
+-+-+ +---+ +---+ +-+-+ C is created (constructor)
| | A is updated (componentDidUpdate)
+-+-+ +-+-+ C did mount (componentDidMount)
| C | | C | B is updated (componentDidUpdate)
+---+ +---+ R is updated (componentDidUpdate)
所以 ReactJS 在进行逐层比较的时候,处理不同层次的结点时只会简单地删除和添加,而对同一层次的结点则会有条件地考虑位置变化。第二种假设影响的便是对同一层次的结点的处理。
每个元素都有可能由唯一的 key 所标识
ReactJS 在处理列表结点时,默认情况下会认为每个元素都有唯一的 key 进行标识,如果没有提供就会提示这样的警告信息:Each child in an array should have a unique "key" prop
,不过这个 key 属性是可选的,界面基本也能正常渲染,只是意味着潜在的性能问题。
具体来说,向一组结点 (A, B, C, D, E, F)
的第二个位置插入一个结点 N
,变成 (A, N, B, C, D, E, F)
。
如果没有提供唯一的 key 属性,那么 ReactJS 会认为 F
元素是一个新的元素,所以会首先创建 F
元素,然后对第二个结点开始的所有结点 (B, C, D, E, F)
执行更新操作:B --> N
,C --> B
,D --> C
,E --> D
,F --> E
,最后把 F
插入到列表中。虽然结果与预期一致,但是其中涉及了不必要的 DOM 操作,从而造成了一定的性能问题。
如果提供 key 属性,那么 ReactJS 会通过内建 Hash Table 高效地计算出最小化 DOM 操作,从而提高渲染效率,所以上述插入新结点的例子所需的 DOM 操作只需把结点 N
插入到结点 A
和结点 B
之间。
+---+ +---+ +---+ +---+ +---+ +---+ F is created (constructor)
| A | | B | | C | | D | | E | | F | A is updated (componentDidUpdate)
+---+ +-+-+ +-+-+ +-+-+ +-+-+ +-+-+ B is updated (componentDidUpdate)
| | | | | C is updated (componentDidUpdate)
| | | | | D is updated (componentDidUpdate)
+-v-+ +-v-+ +-v-+ +-v-+ +-v-+ +---+ E is updated (componentDidUpdate)
| N | | B | | C | | D | | E | | F | F is updated (componentDidUpdate)
+---+ +---+ +---+ +---+ +---+ +---+ F is mounted (componentDidMount)
----------------- KEY PROVIDED ----------------
+---+ +---+ +---+ +---+ +---+ +---+ N is created (contructor)
| A | | B | | C | | D | | E | | F | N is mounted (componentDidMount)
+---+ +---+ +---+ +---+ +---+ +---+ A is updated (componentDidUpdate)
B is updated (componentDidUpdate)
C is updated (componentDidUpdate)
+---+ D is updated (componentDidUpdate)
| N | E is updated (componentDidUpdate)
+---+ F is updated (componentDidUpdate)
+---+ +---+ +---+ +---+ +---+ +---+ F is created (constructor)
| A | | B | | C | | D | | E | | F | A is updated (componentDidUpdate)
+---+ +-+-+ +-+-+ +-+-+ +-+-+ +-+-+ B is updated (componentDidUpdate)
| | | | | C is updated (componentDidUpdate)
| | | | | D is updated (componentDidUpdate)
+-v-+ +-v-+ +-v-+ +-v-+ +-v-+ +---+ E is updated (componentDidUpdate)
| N | | B | | C | | D | | E | | F | F is updated (componentDidUpdate)
+---+ +---+ +---+ +---+ +---+ +---+ F is mounted (componentDidMount)
----------------- KEY PROVIDED ----------------
+---+ +---+ +---+ +---+ +---+ +---+ N is created (contructor)
| A | | B | | C | | D | | E | | F | N is mounted (componentDidMount)
+---+ +---+ +---+ +---+ +---+ +---+ A is updated (componentDidUpdate)
B is updated (componentDidUpdate)
C is updated (componentDidUpdate)
+---+ D is updated (componentDidUpdate)
| N | E is updated (componentDidUpdate)
+---+ F is updated (componentDidUpdate)
因此为列表的每个结点提供唯一的 key 有助于 ReactJS 执行高效的操作,另外这个 key 无需全局唯一,只需保证当前列表内唯一即可。
总结
由于 Diff 算法依赖于这两个假设,所以目前官方给出了三点需要注意的地方:
- Diff 算法不会去对比不同组件之间最终产生的 DOM 结构,如果两个组件之间拥有相似的 DOM 结构,则应该把它们写成同一个组件
key
属性只在当前层次的子树中相邻结点之间有效,如果把这颗子树移动到其他结点,那么key
属性将不起作用,ReactJS 依然会渲染整棵子树key
属性应当稳定、可预测而且唯一,不稳定的key
属性会产生不必要的创建操作,造成性能退化甚至丢失组件的内部 state
另外,由于假设了相同的组件产生类似的 DOM 结构,所以我们应该保持组件的 DOM 结构的稳定性,例如在隐藏/县显示某些结点时,最好通过 CSS 进行隐藏/显示而非移除/添加 DOM 结点。
// recommended
<Component style={{ display: this.state.isShown ? 'block' : 'none' }} />
// bad
{ this.state.isShown ? <Component /> : null }
// recommended
<Component style={{ display: this.state.isShown ? 'block' : 'none' }} />
// bad
{ this.state.isShown ? <Component /> : null }