​我们在每次遍历数据时,都会给一个 key ,这个 key 有什么作用呢?为什么不建议用索引值 index 作为 key 呢?在解密之前我们先了解一下 React 中虚拟 DOM 和 DOM Diffing 算法,再进行“慢动作”演示使用 index 作为 key 会出现什么问题。

与 Diffing 算法关联的两个前端经典面试题

  1. react/vue 中的 key 有什么作用?( key 的内部原理是什么?)
  2. 为什么遍历列表时,key 最好不要用 index ?

在开篇之前,先简介什么是 React 虚拟 DOM

  1. 虚拟 DOM 本质是 Object 类型的对象( JS 中一般对象)。
  2. 虚拟 DOM 比较“轻”(属性少),真实DOM比较“重”(属性很多),因为虚拟 DOM 是 React 内部在用,无需真实 DOM 那么多的属性。
  3. 虚拟 DOM 最终会被 React 转化为真实 DOM ,呈现在页面上。

虚拟 DOM 中 key 的作用

简单地说,key 是虚拟 DOM 对象的标识,在更新显示时 key 起着极其重要的作用。

详细地说: 当状态中的数据发生变化时,React 会根据“新数据”生成“新的虚拟 DOM”,随后 React 进行“新虚拟 DOM”与“旧虚拟 DOM”的 Diffing  比较。

Diffing 比较规则

  • 旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key:
    1. 若虚拟 DOM 中内容没变,直接使用之前的真实 DOM 。
    2. 若虚拟 DOM 中内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 DOM。
  • 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key:
    1. 根据数据创建新的真实 DOM ,随后渲染到到页面。

React 实现与 Dom Diffing 算法

React 不是每一次更新都把页面上的真实 DOM 作出修改,每一个真实 DOM 都对应着一个虚拟 DOM,当新增或修改一个数据,新的虚拟 DOM 就会和旧的虚拟 DOM 进行 Diffing 算法。

image-20221102034608377-1

案例 1 :旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key

如下图中有两份数据,旧的数据添加了一条数据得到一份新的数据,在进行更新遍历时,新旧虚拟 DOM 进行比较,这时 Diffing 算法发现 id 为 1 和 id 为 2 对应的新旧虚拟 DOM 内容没有任何改变,多了一条 id 为 3 的虚拟 DOM,页面上前两条真实 DOM 不做改变,只把 id 为 3 的虚拟 DOM 映射成页面真实 DOM 。

image-20221102034726316-1

案例  2 :旧虚拟 DOM 中找到了与新虚拟 DOM 相同的 key

如下图,旧数据经过修改得到一份新的数据,通过 Diffing 算法更新遍历来修改页面上的真实DOM,新旧虚拟DOM进行比较,发现新旧虚拟 DOM 的 Key 值没有任何改变,但 id 为 2 对应的新旧虚拟的内容发生了改变,则生成新的真实 DOM ,随后替换掉页面中之前的真实 DOM 。

image-20221102035433541

为什么遍历列表时 key 最好不要用 index ?

在 React/Vue 中,我们通常遍历列表,其目的时保证 key 在同级是唯一的。在 React Diffing 算法中 React 会借助元素的 Key 值来判断该元素,从而减少不必要的元素重新渲染。

这个 key 很重要,如果赋值不当会导致:错误更新、性能下降、出 BUG 等问题,有些人习惯在遍历列表时,给 key 赋值列表的索引值 index ,我们来看一看给 key 赋值 index 可能出现的问题吧!

person

如上代码 Person Class ,State(状态)内有一个长度为 2 的 persons 对象数组,在虚拟 DOM 中 map 遍历 persons 对象数组内容, button 按钮 onClick (点击事件) add (事件函数) 来实现添加新用户,通过 setState 调用 render 根据 Diffing 算法来重新渲染页面。

image-20221102044741778
我们来看一看,这是初次打开页面的样子
image-20221102044812474
这是添加一个用户之后的样子,页面根据 Diffing 算法重新渲染,用户 Eric 已经添加上去了

慢动作回放——使用 index 索引值作为 key (如何导致的性能浪费?)

问题来了:添加用户 Eric 前后, key 有什么变化?慢动作回放一下刚刚的操作。

  1. 初始数据:
{id:1,name:'junYan',age:18},
{id:2,name:'Yan',age:19}
  1. 初始的虚拟 DOM :
<li key=0>junYan---18</li>
<li key=1>Yan---19</li>
  1. 更新后的数据:
{id:3,name:'Eric',age:20},
{id:1,name:'junYan',age:18},
{id:2,name:'Yan',age:19}
  1. 更新数据后的虚拟 DOM (注意 key 值更新前后的变化):
<li key=0>Eric---20</li>
<li key=1>junYan---18</li>
<li key=2>Yan---19</li>

我们发现初始的虚拟 DOM 和更新后的虚拟 DOM 内容发生了变化,添加新用户操作,虚拟 DOM 内容更新前后的 key = 0 完全不一样。

按照 Diffing 算法,新旧所有的虚拟 DOM 内容完全不一样,需要替换页面上的所有真实 DOM 和增加新的 DOM ,有没有感觉这很浪费性能?如果你有 1000 条用户数据,添加一个用户数据,这是不是要操作 1001 次页面真实 DOM 呀!

但是,如果使用 id 唯一标识作为 key,我们看看会不会优化性能?


慢动作回放——使用 id 唯一标识作为 key (是如何优化性能的?)

  1. 初始数据:
{id:1,name:'junYan',age:18},
{id:2,name:'Yan',age:19}
  1. 初始的虚拟 DOM:
<li key=1>junYan---18</li>
<li key=2>Yan---19</li>
  1. 更新后的数据:
{id:3,name:'Eric',age:20},
{id:1,name:'junYan',age:18},
{id:2,name:'Yan',age:19}
  1. 更新数据后的虚拟 DOM (注意 key 值更新前后的变化):
<li key=3>Eric---20</li>
<li key=1>junYan---18</li>
<li key=2>Yan---19</li>

我们发现,这次更新前后的虚拟 DOM ,按照 Diffing 算法来说,虚拟 DOM 只新增一条数据,只需把 key = 3 数据创建新的真实 DOM ,随后渲染到到页面就可以了,这样只操作一次页面的真实 DOM 就可以了。

总结: key 最好不要用 index

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作,会产生没有必要的真实 DOM 更新,界面效果没问题, 但效率低。
  2. 如果结构中还包含输入类的 DOM :会产生错误 DOM 更新 ,同时界面也有问题。
  3. 如果只是单纯的展示信息,不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用 index 作为 key 是没有问题的。

总结:在开发中如何选择 key?

  1. 最好使用每条数据的唯一标识作为 key , 比如 id、手机号、身份证号、学号等唯一值。
  2. 如果确定只是简单地展示数据,不做其他的操作,用 index 也是可以的。
_MG_4548--
拍摄于2020 陈俊雁 


个人主页及联系方式,更多交流请联系我:https://chenjunyan1.github.io