Skip to main content

React Key 与 Diff 算法

一、Key 的使用

React 进行列表渲染时,如果没有给渲染列表的每一个子元素添加唯一的 key 值,就会报错:

const numbers = [1, 2, 3, 4, 5]
const listItems = numbers.map((item) => (
<li>{item}</li>
))

// Each child in a list should have a unique "key" prop.

元素的 key 最好是列表中一个独一无二的字符串。通常用数据中的 id 来作为元素的 key。

不建议使用索引来用作 key 值,因为列表项目的顺序可能发生变化,使用索引作 key 值会导致性能变差,还可能引起组件状态的问题。

二、为什么需要 Key

React 与 Vue 一样存在 Diff 算法,而元素的 key 用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染,因此需要用 key 值为每个元素赋予一个确定的标识。

注意:并不是拥有 key 值就性能越高。举个例子:

列表数据渲染中,在后面插入一条数据,key 作用并不大:

this.state = {
numbers: [111, 222, 333]
}

insertMovie() {
const newMovies = [...this.state.numbers, 444];
this.setState({
movies: newMovies
})
}

<ul>
{
this.state.movies.map((item, index) => {
return <li>{item}</li>
})
}
</ul>

在 diff 算法中,前面的元素不产生创建删除操作,元素有无 key 值意义不大。

另外,如果只是文本内容改变,不写 key 反而性能和效率更高,因为不写 key 是对所有的文本内容进行替换,节点发生变化,而写了 key 则涉及到节点的增删,反而增加了性能的开销。

三、diff 算法的定义

React 通过引入虚拟 DOM 的概念,极大地避免了无效的 DOM 操作,使页面的构建效率提到了极大的提升,而 diff 算法就是更高效地通过对比新旧虚拟 DOM 来找出真实 DOM 变化之处。

传统 diff 算法通过循环递归遍历整棵树的节点然后进行比较,效率低下,算法复杂度达到 O(n³),而 React 将算法进行了优化,复杂度降为 O(n),两者效率差距如下图:

上面的 O(n³)O(n) 是怎么算出来的呢?

React 用三大策略将 diff 算法的复杂度从 O(n³) 降为 O(n)

  • 策略一(tree diff)

    只对同一层级节点进行比较,因为 Web UI 中 DOM 节点跨层级的移动操作很少,可以忽略不计。

  • 策略二(component diff)

    两个不同类型的元素会产生出不同的树形结构,如果元素由 <div> 变为 <p>,React 会销毁 <div> 及其子孙节点,并新建 <p> 及其子孙节点。

  • 策略三(element diff)

    对于同一层级的一组子节点,通过唯一 key 值进行区分。

点击查看复杂度讨论详情

四、React diff 优化原理

1、tree 层级

只对同一层级的节点进行比较,DOM 节点跨层级的操作不做优化:

只有删除、创建操作,没有移动操作,如下图:

React 发现新树中,R 节点下没有了 A 节点,则直接删除 A 节点,然后在 D 节点下创建 A 节点及其下属节点,该操作中只有删除和创建操作。

2、conponent 层级

如果是同一类型的组件,则会继续往下进行 diff 运算(可以通过 shouldComponentUpdate() 来控制是否对该组件进行 diff 运算),如果不同类型,则直接删除这个组件下的所有子节点,创建新的:

component D 换成了 component G 后,即使两者的结构相同,也会将 D 删除再重新创建 G。这是因为基本不存在两个不同类但组件结构相同的情况,如果有,只能证明代码复用性不足,需要优化。

3、element 层级

对于同一层级的一组子节点,通过唯一 key 值进行区分。

当节点处于同一层级时,React diff 提供了 3 种节点操作:

  • INSERT_MARKUP (插入)
  • MOVE_EXISTING (移动)
  • REMOVE_NODE (删除)

举个例子,对以下一组节点进行处理:

通过 key 值可以准确发现新旧集合中的节点都是相同的节点,因此无需对节点进行删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置即可。

点击查看 Vue diff 原理