React中map遍历生成列表的时候为何要在列表项中加入key属性?
diff算法
vue中v-for中加key 跟这个也很类似
虚拟DOM的两个假设
1.组件的 DOM 结构是相对稳定的
2.类型相同的兄弟节点可以被唯一标识
然后,我们从react的diff算法开始讲起。react有着一套严密的算法来确保每次组件的所有变动都能及时的得到更新。这套算法不同于标准的Tree Diff算法,建立在以下两个假设的基础上,并将算法复杂度优化到O(n)(标准的Tree Diff算法复杂度为O(n3),意味着如果你的组件中有1000个元素,则需要1000000000次的比对,10亿次,这是啥概念嘛,这个性能是无法承受的):
1、不同的元素类型生产不同的虚拟DOM树。
2、开发人员可以通过不同的key属性来标识哪些子元素在不同的渲染环境中需要保持稳定的。
第二个假设引申意思就是,如果两个元素有不同的key,那么在前后两次渲染中就会被认为是不同的元素,这时候旧的那个元素会被unmount,新的元素会被mount。换言之,如果两个元素是相同的key,且满足第一点元素类型相同,则会被认为是两个相同的元素。这一点不仅仅是适用于在元素遍历的时候,是整个react diff算法的前提。
例如在我们项目中有这样一段代码:
//更新前
render(){
return (
<List key = '1'>
);
}
//更新后
render(){
return (
<List key = '2'>
);
}
这个时候,react就会把这个前一个List销毁之后重新构建一个List的实例(非必要情况下不要这么做,会有额外的性能开销)。
为什么我们在遍历生成元素的时候react要特别警告给元素加上key呢。
遍历生成元素的时候有什么不同
我们在做遍历的时候会返回出一组元素类型相同的子元素。这个过程其实并没有任何问题。问题在于我们对数据进行修改的时候(例如:向数组中插入一个元素,修改一个数组元素的值或则重新排序等),会有很大的不确定性,可能会给react的diff算法带来灾难。因为react在比较元素子元素是否相同的时候并不会精确查找元素具体的位置变动,只会在查到到不同之后对之后所有的元素全部执行一次dom更新操作。
//tree1
<ul>
<li>1</li>
<li>2</li>
</ul>
//tree 2
<ul>
<li>1</li>
<li>3</li>
</ul>
react遇到这种情况的时候,只会修改第二个元素,通过ele.innerHTML = '3'的方法去更新dom,而不会去更新第一个子元素。这种情况下,性能开销会相对较小。但是如果遇到下面的情况,性能开销就大了。
//tree1
<ul>
<li>1</li>
<li>2</li>
</ul>
//tree 2
<ul>
<li>1</li>
<li>3</li>
<li>2</li>
</ul>
在上面的例子中我们试图在tree1中插入一个子元素。这时候react并不会执行插入操作,他直接会移除原先的第二个子元素,然后再append进去剩下的子元素,而其实我们这个操作只只需要一个insert操作就能完成。为了解决这种问题,react需要我们提供给一个key来帮助更新,减少性能开销。
再谈key的作用
在上面的例子中如果我们给每个li元素添加一个key属性情况就会得到优化。
//tree1
<ul>
<li key='1'>1</li>
<li key='2'>2</li>
</ul>
//tree 2
<ul>
<li key='1'>1</li>
<li key='3'>3</li>
<li key='2'>2</li>
</ul>
这个时候react就会通过key来发现tree2的第二个元素不是原先tree1的第二个元素,原先的第二个元素被挪到下面去了,因此在操作的时候就会直接指向insert操作,来减少dom操作的性能开销。
我们要如何选择key
大部分情况下我们要在执行数组遍历的时候会用index来表示元素的key。这样做其实并不是很合理。我们用key的真实目的是为了标识在前后两次渲染中元素的对应关系,防止发生不必要的更新操作。那么如果我们用index来标识key,数组在执行插入、排序等操作之后,原先的index并不再对应到原先的值,那么这个key就失去了本身的意义,并且会带来其他问题。
例如:数组a=['a','b','c'];这个时候原先的0对应的是'a',1对应的是'b',依次...,如果我们对数组进行依次reverse操作,那么这个时候0就对应成了'c',2变成了'a'。这样的导致的除了效率问题还可能会产生额外的bug。具体例子可以点击这里。你可以试着add几条数据之后在input框中输入一些值,然后点下order就会后发现问题所在。
所以我们在选择key的时候一定要选择能和数据一一对应的值。如果找不到这个值可以参考下面操作。
var key = 0;
//可以是任何的id generator
function id(){
return String(++key);
}
//任意的数组或者待遍历的数据
data.forEach((item)=>{
if(!item.id){
item.id = id;
}
})
通过上面的方法我们手动给数组的每个元素添加一个唯一的标识id。
几点提醒
-
key值一定要和具体的元素一一对应到。
-
尽量不要用数组的index去作为key。
-
永远不要试图在render的时候用随机数或者其他操作给元素加上不稳定的key,这样造成的性能开销比不加key的情况下更糟糕。
本文出自https://www.jianshu.com/p/a4ac355ab48c
diff算法
vue中v-for中加key 跟这个也很类似
虚拟DOM的两个假设
1.组件的 DOM 结构是相对稳定的
2.类型相同的兄弟节点可以被唯一标识
然后,我们从react的diff算法开始讲起。react有着一套严密的算法来确保每次组件的所有变动都能及时的得到更新。这套算法不同于标准的Tree Diff算法,建立在以下两个假设的基础上,并将算法复杂度优化到O(n)(标准的Tree Diff算法复杂度为O(n3),意味着如果你的组件中有1000个元素,则需要1000000000次的比对,10亿次,这是啥概念嘛,这个性能是无法承受的):
1、不同的元素类型生产不同的虚拟DOM树。
2、开发人员可以通过不同的key属性来标识哪些子元素在不同的渲染环境中需要保持稳定的。
第二个假设引申意思就是,如果两个元素有不同的key,那么在前后两次渲染中就会被认为是不同的元素,这时候旧的那个元素会被unmount,新的元素会被mount。换言之,如果两个元素是相同的key,且满足第一点元素类型相同,则会被认为是两个相同的元素。这一点不仅仅是适用于在元素遍历的时候,是整个react diff算法的前提。
例如在我们项目中有这样一段代码:
//更新前
render(){
return (
<List key = '1'>
);
}
//更新后
render(){
return (
<List key = '2'>
);
}
这个时候,react就会把这个前一个List销毁之后重新构建一个List的实例(非必要情况下不要这么做,会有额外的性能开销)。
为什么我们在遍历生成元素的时候react要特别警告给元素加上key呢。
遍历生成元素的时候有什么不同
我们在做遍历的时候会返回出一组元素类型相同的子元素。这个过程其实并没有任何问题。问题在于我们对数据进行修改的时候(例如:向数组中插入一个元素,修改一个数组元素的值或则重新排序等),会有很大的不确定性,可能会给react的diff算法带来灾难。因为react在比较元素子元素是否相同的时候并不会精确查找元素具体的位置变动,只会在查到到不同之后对之后所有的元素全部执行一次dom更新操作。
//tree1
<ul>
<li>1</li>
<li>2</li>
</ul>
//tree 2
<ul>
<li>1</li>
<li>3</li>
</ul>
react遇到这种情况的时候,只会修改第二个元素,通过ele.innerHTML = '3'的方法去更新dom,而不会去更新第一个子元素。这种情况下,性能开销会相对较小。但是如果遇到下面的情况,性能开销就大了。
//tree1
<ul>
<li>1</li>
<li>2</li>
</ul>
//tree 2
<ul>
<li>1</li>
<li>3</li>
<li>2</li>
</ul>
在上面的例子中我们试图在tree1中插入一个子元素。这时候react并不会执行插入操作,他直接会移除原先的第二个子元素,然后再append进去剩下的子元素,而其实我们这个操作只只需要一个insert操作就能完成。为了解决这种问题,react需要我们提供给一个key来帮助更新,减少性能开销。
再谈key的作用
在上面的例子中如果我们给每个li元素添加一个key属性情况就会得到优化。
//tree1
<ul>
<li key='1'>1</li>
<li key='2'>2</li>
</ul>
//tree 2
<ul>
<li key='1'>1</li>
<li key='3'>3</li>
<li key='2'>2</li>
</ul>
这个时候react就会通过key来发现tree2的第二个元素不是原先tree1的第二个元素,原先的第二个元素被挪到下面去了,因此在操作的时候就会直接指向insert操作,来减少dom操作的性能开销。
我们要如何选择key
大部分情况下我们要在执行数组遍历的时候会用index来表示元素的key。这样做其实并不是很合理。我们用key的真实目的是为了标识在前后两次渲染中元素的对应关系,防止发生不必要的更新操作。那么如果我们用index来标识key,数组在执行插入、排序等操作之后,原先的index并不再对应到原先的值,那么这个key就失去了本身的意义,并且会带来其他问题。
例如:数组a=['a','b','c'];这个时候原先的0对应的是'a',1对应的是'b',依次...,如果我们对数组进行依次reverse操作,那么这个时候0就对应成了'c',2变成了'a'。这样的导致的除了效率问题还可能会产生额外的bug。具体例子可以点击这里。你可以试着add几条数据之后在input框中输入一些值,然后点下order就会后发现问题所在。
所以我们在选择key的时候一定要选择能和数据一一对应的值。如果找不到这个值可以参考下面操作。
var key = 0;
//可以是任何的id generator
function id(){
return String(++key);
}
//任意的数组或者待遍历的数据
data.forEach((item)=>{
if(!item.id){
item.id = id;
}
})
通过上面的方法我们手动给数组的每个元素添加一个唯一的标识id。
几点提醒
-
key值一定要和具体的元素一一对应到。
-
尽量不要用数组的index去作为key。
-
永远不要试图在render的时候用随机数或者其他操作给元素加上不稳定的key,这样造成的性能开销比不加key的情况下更糟糕。
本文出自https://www.jianshu.com/p/a4ac355ab48c
官网 协同章节
设计动力
在某一时间节点调用 React 的 render()方法,会创建一颗由 React 元素组成的树.在下一次 state 或者 props 更新的时候,相同的 render()方法会返回一棵不同的树.React需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前的 UI 与最新的树保持同步.
目前最牛逼的算法,将一棵树转换成另一颗树的时间复杂度仍然为 O(n^3),倘若 1000 个元素...
Diffing算法
-
对比不同类型的元素
当根节点为不同类型的元素的时候,React 会拆卸原有的树并建立起新的树。举个例子,当一个元素从<a>变成<img>,从<Article>变成<Comment>,或者从<Button>变为<div>都会触发一个完整的重建流程。
当拆卸一棵数的时候,对应的 DOM 节点也会被销毁。组件实例将执行 componentWillUnmount()方法。当监理一棵新的树的时候,对应 DOM 节点会被创建以及插入到 DOM 中。组件实例将执行 componentWillMuount()方法,紧接着 componentDidMount()方法。所有之前的树所关联的 state 也会被销毁。
在根节点一下的组件也会被销毁,它们的状态 state 也会被销毁。比如,当比对以下变更的时候:
<div> <Counter /> </div> <span> <Counter /> </span>
-
对比同一类型的元素
当对比两个相同类型的 React 元素的时候,React 会保留 DOM 节点,仅对比和更新有改变的属性
<div className="before" title="stuff"/> <div className="after" title="stuff"/>
对比这两个元素,React 知道只需要修改 DOM 元素上的 className 属性。比如:
<div style={{color:'red', fontWeight: 'bold'}} /> <div style={{color:'green', fontWeight: 'bold'}} />
通过对比着两个元素,React知道需要修改 DOM 元素上的 color 样式,无需修改 fontWeight
在处理完当前节点之后,React 继续对子节点进行递归
-
对比同类型的组件元素
当一个组件更新的时候,组件实例保持不变。这样 state 在跨越不同的渲染时保持一致。React 将更新该组件实例的 props 以跟最新的元素保持一致,并且调用该实例的 componentWillReceiveProps() 和 componentWillUpdate()方法。
下一步,调用 render()方法,diff 算法将在之前的结果以及新的结果中进行递归
-
对子节点进行递归
在默认条件下,当递归 DOM 节点的子元素的时候,React 会同时遍历两个子元素的列表;当产生差异的时候,生成一个 mutation
在子元素列表末尾新增元素的时候,变更的开销会变的比较小,比如:
<ul> <li>first</li> <li>second</li> </ul> <ul> <li>first</li> <li>second</li> <li>third</li> </ul>
React会匹配两个
- first 对应的树,然后匹配第二个元素
- second 对应的树,最后插入第三个元素的
- third 树。
- Duke 和
- Villanova 子树完成。这种情况下的低效可能会带来性能问题。
-
keys
为了解决上面的问题,React 支持 key 属性。当子元素拥有 key 时,React 使用 key 来匹配原有的树上的子元素以及最新的树上的子元素。一下例子在新增 key 之后使得之前的低效变为高效:
<li key="2015">Duke</li> <li key="2016">Villanova</li> </ul> <ul> <li key="2014">Connecticut</li> <li key="2015">Duke</li> <li key="2016">Villanova</li> </ul>
现在 React 知道只有带着’2014’ key 的元素是新增的元素,带着’2015’以及’2016‘key 的元素仅仅移动了
显示场景中,产生一个 key 并不困难,你要展现的元素可能已经有了一个唯一的 ID,于是 key 可以直接从你的数据里面获取:
<li key={item.id}>{item.name}</li>
当以上的情况不成立的时候,你可以新增一个 ID 字段到你的模型里面,或者利用一部分的内容作为哈希值来生成一个 key。这个 key 不需要全局唯一,但是在列表里面需要保持唯一。
最后,你也可以使用元素在数组当中的下标作为 key。这个策略在元素不进行重新排序的时候比较合适,一旦有顺序修改,diff 就会变得很慢。
当基于下表的组件进行重新排序的时候,组件 state 可能就会遇到一些问题。由于组件实例是基于它们的 key 来决定是否更新以及复用,如果一个 key 是一个下标,那么修改顺序的时候会修改当前的 key,导致非受控组件的 state(比如输入框)可能相互篡改导致无法预期的变动。
-
权衡
请谨记协调算法是实现的一个细节。React 可以在每个 action 之后对整个应用进行重新渲染,得到的最终结果也会是一样的。在此情境下,重新渲染表示在所有组件内调用 render 方法,这并不代表 React 会卸载或者装载它们。React 只会基于以上提到的规则来决定如何进行差异的合并。
我们定期优化搜索算法,让常见用例更高效地执行。在当前的实现中,可以理解为一个子树只能在兄弟之间移动,但是不能移动到其他的位置。这种情况下,算法会重新渲染整颗的子树。
由于 React 依赖探索的算法,因此当一下假设没有满足的时候,性能就会有所损耗。
- 该算法不会尝试匹配不同组件类型的子树。如果你发现你在两种不同类型的组件中切换,但是输出非常相似的内容,建议把他们改成同一种类型。在实践中,我们没有遇到这类的问题。
- Key 应该具有稳定性,可以预测,以及列表内唯一。不稳定的 key(比如通过 Math.random()生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这会导致性能下降以及组件中状态的丢失。
如果简单实现的话,那么在列表头部插入会很影响性能,那么更变开销会比较大。比如:
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
React会针对每个子元素 mutate 而不是保持相同的