发布网友 发布时间:2024-09-17 10:25
共1个回答
热心网友 时间:2024-10-01 19:07
我们在开发React项目时,根据规范,对于列表、表格类型的元素,每项需要指定key属性,否则会出现warning报错。正如antdesign的Table组件文档中描述的那样,当不指定key时,可能会出现未知错误。
我们需要尽量给每条数据提供一个key属性,在实际项目中,一般数据是从后台获取的,所以我们可以使用唯一的标识id作为key值。但如果数据没有id属性的话,也不推荐使用index作为key。React官网上也有相关的解释说明,不建议使用index作为key,可以去深入了解一下。本文将讨论不推荐的原因,主要分为2点。
Wedon’trecommendusingindexesforkeysiftheorderofitemsmaychange.Thiscannegativelyimpactperformanceandmaycauseissueswithcomponentstate.CheckoutRobinPokorny’sarticleforanin-depthexplanationonthenegativeimpactsofusinganindexasakey.IfyouchoosenottoassignanexplicitkeytolistitemsthenReactwilldefaulttousingindexesaskeys.
性能在讨论性能问题前,我们先看一个案例。
functionApp(){const[data,setData]=useState([{name:'Tom',id:'id1'},{name:'Sam',id:'id2'},{name:'Ben',id:'id3'},{name:'Pam',id:'id4'},]);return(<divclassName="App"><ul>{data.map(({name},index)=>(<likey={index}>{name}</li>))}</ul><br/><br/><buttononClick={()=>{constnewData=[...data];newData.shift();setData(newData);}}>删除第一项</button></div>);}在这个案例中,我们使用map函数进行生成多个li标签,每个标签的key值用index进行绑定。当点击按钮的时候,会将data中的第一项进行删除,触发重新渲染。
从截图中可以看到,确实可以达到我们想要的效果,但是如果我们细究一下React实际渲染更新做的事情,会发现效率不高。
<!--更新前--><likey="0">Tom</li><likey="1">Sam</li><likey="2">Ben</li><likey="3">Pam</li><!--更新后--><likey="0">Sam</li><likey="1">Ben</li><likey="2">Pam</li>我们知道React更新机制是先比较虚拟DOM,然后通过计算差异,再对真实DOM进行操作。
如上述代码所示,更新前后,index依旧从0开始,React进行逐条比较,发现了2条同样key=0的li标签,然后递归比较内部,发现内部的文字由Tom改为了Sam,因此需要找到key=0的li标签,并进行真实DOM操作将内部的文字改为Sam,此时完成本条数据的更新。然后继续比较key=1、key=2的数据,并进行更新。最后,原本的key=3的li标签在新的虚拟DOM中,已经不存在了,于是执行了DOM删除。
<!--更新前--><likey="id1">Tom</li><likey="id2">Sam</li><likey="id3">Ben</li><likey="id4">Pam</li><!--更新后--><likey="id2">Sam</li><likey="id3">Ben</li><likey="id4">Pam</li>如果我们使用数据的id作为key,一切就不一样了。React一开始就发现key=id1的数据没有了,就会进行删除的操作。而其他3条数据,在进行比较后,会发现无变化,因此不会产生真实DOM的更新。
小结一下,在这个案例中,我们想要将第一条数据进行删除,触发页面上的元素变化。如果我们使用index作为key,会导致所有的真实DOM都发生变化;如果我们使用id作为key,则可以保证最小代价的更新,效率更高。
更新不符预期如果只是效率问题,可能不是我们需要优先解决的,但是如果更新也会出错的话,那我们就无法忽视了。这里会列举2个例子来说明该问题。
输入框内容functionApp(){const[data,setData]=useState([{name:'Tom',id:'id1'},{name:'Sam',id:'id2'},{name:'Ben',id:'id3'},{name:'Pam',id:'id4'},]);return(<divclassName="App"><ul>{data.map(({name},index)=>(<likey={index}>{name}<input/></li>))}</ul><br/><br/><buttononClick={()=>{constnewData=[...data];newData.shift();setData(newData);}}>删除第一项</button></div>);}类似之前的案例,但在遍历数据时,这次我们增加一个输入框的渲染。测试案例时,我们首先在每个输入框内输入各自的内容,然后再点击按钮删除第一项。
可以发现,看起来第一项Tom确实被删除了,但是input标签内的输入值依旧保持为1。
<!--更新前--><likey="0">Tom<input/></li><likey="1">Sam<input/></li><likey="2">Ben<input/></li><likey="3">Pam<input/></li><!--更新后--><likey="0">Sam<input/></li><likey="1">Ben<input/></li><likey="2">Pam<input/></li>原因也很容易理解,正如之前的例子一样,虽然看起来是第一项被删除了,但实际上,React在计算差异时,最终删除的其实是最后一项。看起来是第一条被删除的原因是,React递归的比较内部的差异,然后更新了文字内容。而从结果上,我们也可以发现input标签在前后比较中,被认为未发生变化,因此输入值依旧保留着我们更新前输入的内容。而最后一项li标签被删除,所以input标签也同时被删除了。
文字标记有时候,我们会在网页中进行一些标记,类似划词翻译、主体识别等需求,当涉及数据修改的情况,使用index作为key容易出现问题。
functionApp(){const[data,setData]=useState([{name:'Tom',id:'id1'},{name:'Sam',id:'id2'},{name:'Ben',id:'id3'},{name:'Pam',id:'id4'},]);return(<divclassName="App"><ul>{data.map(({name},index)=>(<likey={index}className="my-list-item">{name}<input/></li>))}</ul><br/><buttononClick={()=>{constfirstItem=document.getElementsByClassName('my-list-item')[0];firstItem.innerHTML=firstItem.innerHTML.replace('m','<a>m</a>');}}>标记第一项</button><br/><buttononClick={()=>{constnewData=[...data];newData.shift();setData(newData);}}>删除第一项</button></div>);}在上述案例中,我们把第一项中的m进行标记,通过增加a标签包裹的形式,此时进行删除第一项会发现严重的更新错误。
我们可以发现第一项并没有被删除,反而是第二项被删除了。
<!--更新前--><likey="0">Tom<input/></li><likey="1">Sam<input/></li><likey="2">Ben<input/></li><likey="3">Pam<input/></li><!--标注后--><likey="0">To<a>m</a><input/></li><likey="1">Sam<input/></li><likey="2">Ben<input/></li><likey="3">Pam<input/></li><!--更新后--><likey="0">Sam<input/></li><likey="1">Ben<input/></li><likey="2">Pam<input/></li>我们知道React更新的时候会首先比较虚拟DOM,然后计算如何更新,再将更新过程映射成真实DOM的操作,并最终完成更新。标注行为在这个案例中是直接通过修改DOM的形式进行操作的,React并不知道发生了变化,因此在计算虚拟DOM变化的时候,依旧是用的<likey="0">Tom<input/></li>而不是<likey="0">To<a>m</a><input/></li>。在更新前,此处还有个特殊点在于,li标签下的Tom被视为一个隐式的节点,当发生更新的时候,预期的操作是,将该隐式节点替换成Sam。然而经过标注后,节点被破坏了,变成了To和span2个节点。找不到Tom节点导致React无法完成预期的更新操作。
如果希望解决这样的问题,可以试着将input标签去除,此时由于li标签只有一个子节点Tom,因此React可以直接修改li的innerHTML完成更新。或者我们保留input标签,但是给Tom外层增加一个span标签,此时React更新的时候会修改li内第一个标签,即span标签进行更新。
当然最好还是避免使用index作为key!
小结一下,在这两个案例中,发生更新的元素内由于存在无法被判断变化的元素,比如input,或者元素发生了React无法预知的修改导致与虚拟DOM不再匹配。在更新元素时,会出现更新不符预期的情况。
总结不推荐使用index作为key,除非不涉及任何更新修改
使用index作为key会导致渲染效率的问题
使用index作为key会导致更新不符预期的问题
如果读者有兴趣的话,可以去深度了解一下React的diffing算法。
本文所用代码可在本人仓库找到:gitee
原文:https://juejin.cn/post/7099745927413366797