前言
将由两个简单实例对比两种赋值key的方式,向大家展示使用index作为key出现的bug,和正确赋值key的区别所在。并对此进行分析。
用index做下标情况
假设有一个数组:
list = ['a','b','c','d','e']
初学者们在写react时对数组进行遍历时喜欢这样写:
list.map((item, index) => {
return <li key={index}>{item}</li>
})
写vue时对数组遍历:
<ul>
<li v-for="(item, index) in list" :key="index">
{{ item }}
</li>
</ul>
这样写似乎不会报错?的确,这样写确实没有任何语法上的问题。
但是,一旦我们对遍历好的item项再做操作,结果与我们想象的将会大不相同。
我用React写一个结构简单的例子小伙伴们就秒懂了(●´∀`●):
用vue写原理也是一样的,我们不必局限于所使用的框架
代码如下:
class Items extends React.Component {
constructor(props) {
super(props)
this.state = {list: ['a','b','c','d','e']}
}
render() {
let list = this.state.list
return (
<ul>
{list.map((item, index) => (
<li
key={index}
onClick={this.deleteItem.bind(this, index)}
>
{item}
</li>
))}
</ul>
)
}
// 删除点击的item
deleteItem(index) {
let newlist = [...this.state.list]
newlist.splice(index, 1)
this.setState(()=>({list: newlist}))
}
}
这些代码会生成这样一个列表:
我们在代码中给每个item绑定了一个deleteItem事件,即点击某个item便会删除对应的item。
好了,我们演示一下点击的效果:
这时有小伙伴就说了,你弄了半天想说明啥,,这效果有问题吗?
我想说,这效果没问题,,,But!每次点击删除对应li标签后最终浏览器对dom的操作大有问题!
我们点击第一个item(文本为a)看似删除了它,但是实际上浏览器做了什么呢?
浏览器将最后一个item删除了!
你会想我明明看到a被删除了,接着是b被删除,再是c....
让我们一起来看右边的控制台。每次点击第一个li标签,发现dom结构中ul标签以及剩余的所有li标签都发生了闪烁!
浏览器竟然重新渲染了所有的li标签!
点击删除一个节点后React的内部操作:
无论是vue还是react其内部都用虚拟Dom(VDom)机制来更新浏览器中真实的Dom。
如何判断新旧虚拟DOM的差异呢,这就用到了diff算法。
在此案例中当我们删除了第一个节点,那么react就会生成一个新的虚拟DOM(newVDom),为了问题简单化我们只针对列表部分进行分析。
<!-- 真正的虚拟DOM是js中的对象。 -->
<!-- newVDom -->
<ul>
<li>b</li> <!-- key==0 -->
<li>c</li> <!-- key==1 -->
<li>d</li> <!-- key==2 -->
<li>e</li> <!-- key==3 -->
</ul>
<!-- oldVDom -->
<ul>
<li>a</li> <!-- key==0 -->
<li>b</li> <!-- key==1 -->
<li>c</li> <!-- key==2 -->
<li>d</li> <!-- key==3 -->
<li>e</li> <!-- key==4 -->
</ul>
diff算法将newVDom与改变前的Dom结构(oldVDom)进行比较,我们找到key值相同的li标签,并进行自上至下逐一对比。对比发现:
newVDom | oldVDom | 变化 |
---|---|---|
key==0 | key==0 | 文本改变 |
key==1 | key==1 | 文本改变 |
key==2 | key==2 | 文本改变 |
key==3 | key==3 | 文本改变 |
无 | key==4 | 删除节点 |
- key为0的li标签文本由原先的a变为了b
- key为1的li标签文本由原先的b变为了c
- key为2的li标签文本由原先的c变为了d
- key为3的li标签文本由原先的d变为了e
- 最后key为4的标签被移除
在react中,只有被改变了的元素才会被重新渲染
1.diff将上面的所有比对的差异放入一个补丁包中,再将补丁包应用到真实DOM上。
2.原本我们每次点击只需要逐个删除列表中的第一个li节点
3.由于用index做key导致点击事件发生后,新产生的newVDOM中li的key被动态赋值,于是就有了上面对比错位,导致diff以为每个li都发生了改变,于是页面重新渲染了所有的列表项。
用item做下标情况
修改代码:
list.map((item, index) => (
<li
key={item}
onClick={this.deleteItem.bind(this, index)}
>
{item}
</li>
)
暂且让item成为key(当然在做项目时这仍不可取)。于是每个li都独有一个key,值分别为'a','b','c','d','e'。 再次演示:
可见每点击一次,生成相对应的newVDom就会少一个节点,进入diff算法进行新旧VDom的比较时就会得出结果:仅仅是删除了一个节点。再由newVDom映射到真实的Dom,只做了一个删除dom的操作,并没有重新渲染其他li,极大提升了性能。
小结
无论是Vue还是React,当我们在做数组遍历批量生成子节点时,切记同层级的每个子节点的key值不能重复且不会发改变,否则将会产生不可预估的bug。