使用Perf工具研究React Key对渲染的影响

使用React的开发版本时常会遇到这样的情况:

渲染列表时, 不为数组的每一项设置key, 则控制台会警告

Warning: Each child in an array or iterator should have a unique “key” prop.
Check the render method of Constructor.
See http://fb.me/react-warning-keys for more information.

我常看到有的代码为了消除警告, 把数组的下标(index)作为key, 那么这倒底是不是一种好的做法呢, 本文将一探究竟.

准备工作

在页面引入react-with-addons.js, 可以去bootcdn随便找一个.

进行本文的实验时, 我使用的是0.13.1版本. 后面我又当前最新版本15.3.1重新试过, 实验结果是一致的, 只是输出的信息在描述上有些差异, 我就没重新截图了.

背景知识

1.列表初始化

列表一开始长这个样子

2.列表项信息

列表项DOM结构如下

1
2
3
4
5
<div>
<span className={'checkbox ' + (data.isDefault ? 'active' : '')} ref="checkbox"/>
<span>{data.id}</span>
<span className="delete" ref="delete">删除</span>
</div>

每一个列表项都可以做两个操作:

  1. 点击checkbox, 设置该项为默认列表项.
  2. 点击删除, 删除该项

以上操作都触发列表刷新, 即重新获取列表数据;

返回的数据结构为数组, 数组的第一项是默认项;
每一个列表项的id都是唯一的

3.Reconciliation

When a component’s props or state change, React decides whether an actual DOM update is necessary by constructing a new virtual DOM and comparing it to the old one. Only in the case they are not equal, will React reconcile the DOM, applying as few mutations as possible

每当组件的propsstate改变时, React会重新创建一个virtual DOM, 与上一个作对比, 如果发现两个virtual DOM不完全相同, 则React就会做reconcile(姑且翻译成”较正”吧), 把有差异的地方更新到真实的DOM上(这当然是建立在shouldComponentUpdate方法返回true的前提下, 那又是另一个话题了).

4.Perf工具的基本使用

打开浏览器的控制台:

  1. 执行操作前, 调用 React.addons.Perf.start()
  2. 执行操作后, 调用 React.addons.Perf.stop()
  3. 第2步之后, 调用 React.addons.Perf.printDOM() 相看React对DOM的操作情况

第3步, 如果用更高版本的React, 可能会让你使用React.addons.Perf.printOperations

更多方法请看官方文档


进行实验

改变默认项

点击第二个checkbox, 设置id为39的列表项为默认项, 观察列表重新渲染的结果

1.使用id作为key

渲染结果与后台返回数据一致

使用Perf工具得到的结果:

2.使用index作为key

渲染结果与后台数据不完全一致, 表现为checkbox选中的情况不对

使用Perf工具得到的结果:

删除列表项

删除列表的第一个项目, 看看React是怎么渲染的

1.使用id作为key

调整列表顺序, 让id为37的列表项在前面, 然后点击删除(则列表只剩下id为39的列表项)

为方便说明, 表示如下:

1
[{key: '37', text: '37'}, {key: '39', text: '39'}] => [{key: '39', text: '39'}]

使用Perf工具得到的结果:

可以看出React直接把key为37的项目删除掉了

2.使用index作为key

新建一个列表项, id为46(id不受我控制, 我也想40, 囧…)

调整列表顺序, 让id为39的列表项在前面, 然后点击删除

为方便说明, 表示如下:

1
[{key: '0', text: '39'}, {key: '1', text: '46'}] => [{key: '0', text: '46'}]

使用Perf工具得到的结果:

上图表明, React先把key为0的项目, 再删除key为1的项目.

换句话说, 如果这个列表有10条数据, 则React会先更新前面9条数据, 再删除第10条数据.

结论

使用React渲染列表时, 把数组的下标作为key是不正确的, 它能做的, 仅仅是消除警告而已, 对渲染的毫无帮助.

React的key应该是稳定的, 唯一的(只要在列表项中唯一就行). 把数组的下标作为key的缺点在于没有满足稳定性要求

推荐把id或uuid等字段作为React的key, 减少React对DOM操作

另: 相同的key, 只有第一个节点会被渲染
Encountered two children with the same key, '.0:$1'. Child keys must be unique; when two children share a key, only the first child will be used.

本文好像重点关注性能, 其实做这个研究只是想消除心中的疑惑, 因为我想知道用不同的方案设置key, 对React的渲染到底有没有影响.

至于性能方面, 只是因为涉及到了这方的知识, 所以顺带一提. 正如PERFORMANCE ENGINEERING WITH REACT提到的: “如果你并非在做大型而复杂的应用, 你不需要过早地进行性能优化, 还是先把应用做出来吧”

参考资料

  1. Reconciliation
  2. Avoiding reconciling the DOM
  3. React 实践心得:key 属性的原理和用法
  4. react组件性能优化探索实践
  5. Index as a key is an anti-pattern
  6. Performance Tools
  7. PERFORMANCE ENGINEERING WITH REACT

附: 代码清单

列表组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const List = React.createClass({
getInitialState() {
return {
list: []
}
},
componentWillMount() {
this.getList();
},
render() {
return (
<div>
{this.state.list}
</div>
)
},
getList() {
ajax({
url: api.getList,
success: data => {
this.setState({
list: data.map((item, index) => {
// 如果key一样, 只渲染第一个元素
// 不设置key, 效果与以数组下标为key相同
return <Item key={item.id} // 这里会根据实验需要,改成key={index}
data={item}
getList={this.getList}/>
})
})
}
});
}
})

列表项组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
const Item = React.createClass({
componentDidMount() {
let $checkbox = $('.checkbox'),
me = this;
// 点击checkbox
R.findDOMNode(this.refs.checkbox).addEventListener('touchend', function () {
let $me = $(this);
if(!$me.hasClass('active')) {
// 先改变checkbox的样式
$me.toggleClass('active');
$checkbox.not($me).removeClass('active');
// 设置default Item
ajax({
type: 'post',
url: api.setDefault,
data: {
id: me.props.data.id,
},
success() {
// 重新获取列表数据, 此时后台数据中的第一项为 default Item;
me.props.getList()
}
});
}
});
// 点击删除
R.findDOMNode(this.refs.delete).addEventListener('touchend', () => {
confirm('确认删除吗?', () => {
ajax({
type: 'post',
url: api.delete,
data: {
id: me.props.data.id,
},
success() {
// 重新获取列表数据
me.props.getList()
}
})
})
});
},
render() {
const data = this.props.data;
return (
<div>
<span className={'checkbox ' + (data.isDefault ? 'active' : '')} ref="checkbox"/>
<span>{data.id}</span>
<span className="delete" ref="delete">删除</span>
</div>
)
}
})
Fork me on GitHub