轻松理解为什么不用Index作为key

·  阅读 6314
轻松理解为什么不用Index作为key

前言

将由两个简单实例对比两种赋值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标签,并进行自上至下逐一对比。对比发现:

newVDomoldVDom变化
key==0key==0文本改变
key==1key==1文本改变
key==2key==2文本改变
key==3key==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。

分类:
前端
标签:
分类:
前端
标签: