一个改变的key引发的问题
今天闲来无事,忽然想起了前天在开发功能时遇到的一个问题,具体情况就是我渲染了一个邮箱列表,列表支持修改邮箱,但是发现每次改完邮箱后input框就会失去焦点,按理说input是不会失去焦点的,但bug就此发生了。
需要渲染的邮箱列表数据格式 ['xiaoming@163.com','lier@163.com']
,渲染的列表支持修改,代码如下:
1 | render(){ |
看完上面是否有发现问题。大体看上去没啥问题,但是在页面上实际测过后你会发现一个bug,每次敲击完键盘,input输入框会失去焦点,那么为什么会发生这种bug呢,其实你回想下react的渲染流程应该就会发现问题。
先说原因:因为你每次change后email发生了改变,也就是说,你在渲染列表的时候,列表的 key
发生了改变,此时再次渲染,原来的dom已经被干掉换成新的dom了,只是看起来还是原来的那个input;
再说处理办法:慎重定义 key
的值
再说为啥:键(keys)帮助React标识哪项被修改,添加,删除官方文档;
来看下官方的解释官方文档
当你使用 React ,在任何一个单点时刻你可以认为
render()
函数的作用是创建 React 元素树。在下一个state
或props
更新时,render()
函数将会返回一个不同的 React 元素树。接下来 React 将会找出如何高效地更新 UI 来匹配最近时刻的 React 元素树。
目前有很多通用的做法来尽可能的减少将一个dom树转换成另外一个dom树,但是这些方法的复杂度却是O(n3)
。也就是说,如果你有1000个元素要更新,那么每次都要进行10亿次的比较,这个开销还是很巨大的,试想如果React真有这么大的开销,那他就不是React了。然而React基于以下两个假设,将复杂度降到了O(n)
:
- 不同类型的两个元素会产生两个不同的树
- 开发人员可以使用一个
key
来指示在不同的渲染中哪些元素可以保持稳定
Diff算法
当比较两个树的时候,React首先会比较两个根元素,依据类型的不同会有不同的处理。
根元素类型不同
任何时候,元素的类型不同React都会销毁dom,然后重新构建新的dom。比如a
标签换成img
元素类型相同
当比较两个相同类型的 React DOM 元素时,React 检查它们的属性(attributes),保留相同的底层 DOM 节点,只更新反生改变的属性(attributes)。组件类型相同
react在更新组件的时候,组件实例保持不变,这样可以保持组件的state不变。react会更新组件实例的属性来匹配新的元素。并在实例上调用componentWillReceiveProps()
和componentWillUpdate()
。
接下来,render()
方法会被调用,并且diff算法对上一次的结果和新的结果进行递归。子元素递归
在遍历节点的字节点时,一般直接遍历所有子节点就可以。但是在react中这种遍历有时就会有不同的影响1
2
3
4
5
6
7
8
9
10
11<!-- dom1 -->
<ul>
<li>first</li>
<li>second</li>
</ul>
<!-- dom2 -->
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>React 会遍历ul下子节点,first,second没有改变,只在末尾插入third即可,性能良好。但是结构变更一下
1
2
3
4
5
6
7
8
9
10
11<!-- dom1 -->
<ul>
<li>first</li>
<li>second</li>
</ul>
<!-- dom2 -->
<ul>
<li>third</li>
<li>first</li>
<li>second</li>
</ul>此时,react遍历ul子节点,发现first变成了third,此时会重新渲染这些li,此时性能就会贼差。
针对以上问题,react并不会意识到li标签只是新增了一个标签而且,此时并不需要重新渲染这些li。
Keys 横空出世
key的值必须保证唯一且稳定
为了解决这个问题,React 支持一个key
属性(attributes)。当子节点有了key
,React 使用这个key
去比较原来的树的子节点和之后树的子节点。
同样的结构:
1 | <!-- dom1 --> |
react 遍历子节点,此时只需要比较key
的差异即可,发现只是新增third,此时并不会渲染三个li,只是插入
1 | <li key="third">third</li></code> |
即可