Skip to content

前言

我们在用 React 或者是 Vue 开发的时候,有一个遍历列表的操作,通常都会去绑定一个 key 值,那这个 key 有什么用呢?为什么遍历列表的时候,key 最好不要用 index?

今天我们以 React 为基础去了解一下 React 虚拟 DOM 中的 Key 和 index。

虚拟 DOM 中的 Key 作用

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

详细的说:当状态中的数据发生变化时,react 会根据【新数据】生成【新的虚拟 DOM】, 随后 React 进行【新虚拟 DOM】与【旧虚拟 DOM】的 diff 比较,比较规则如下:

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

示例代码

为了方便计较,我把 index 作为 key 值和用 id 作为 key 放到一起了,来通过一段代码来看一下效果:

js
import React, { Component } from "react";

export default class example17 extends Component {
  state = {
    persons: [
      { id: 1, name: "小张", age: 18 },
      { id: 2, name: "小李", age: 19 },
    ],
  };

  add = () => {
    const { persons } = this.state;
    const p = { id: persons.length + 1, name: "小王", age: 20 };
    this.setState({ persons: [p, ...persons] });
  };

  render() {
    return (
      <div>
        <h2>展示人员信息</h2>
        <button onClick={this.add}>添加一个小王</button>
        <h3>使用index(索引值)作为key</h3>
        <ul>
          {this.state.persons.map((personObj, index) => {
            return (
              <li key={index}>
                {personObj.name}---{personObj.age}
                <input type="text" />
              </li>
            );
          })}
        </ul>
        <hr />
        <hr />
        <h3>使用id(数据的唯一标识)作为key</h3>
        <ul>
          {this.state.persons.map((personObj) => {
            return (
              <li key={personObj.id}>
                {personObj.name}---{personObj.age}
                <input type="text" />
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}
import React, { Component } from "react";

export default class example17 extends Component {
  state = {
    persons: [
      { id: 1, name: "小张", age: 18 },
      { id: 2, name: "小李", age: 19 },
    ],
  };

  add = () => {
    const { persons } = this.state;
    const p = { id: persons.length + 1, name: "小王", age: 20 };
    this.setState({ persons: [p, ...persons] });
  };

  render() {
    return (
      <div>
        <h2>展示人员信息</h2>
        <button onClick={this.add}>添加一个小王</button>
        <h3>使用index(索引值)作为key</h3>
        <ul>
          {this.state.persons.map((personObj, index) => {
            return (
              <li key={index}>
                {personObj.name}---{personObj.age}
                <input type="text" />
              </li>
            );
          })}
        </ul>
        <hr />
        <hr />
        <h3>使用id(数据的唯一标识)作为key</h3>
        <ul>
          {this.state.persons.map((personObj) => {
            return (
              <li key={personObj.id}>
                {personObj.name}---{personObj.age}
                <input type="text" />
              </li>
            );
          })}
        </ul>
      </div>
    );
  }
}

效果图:

key和index.gif

慢动作回放----使用 index 索引值作为 key

上面效果图中,明明是给小李---19的输入框中输入了周星星,却在添加小王后,周星星跑到了小张---18

这是为什么呢?我们来分析一波:

初始数据:

js
{id:1,name:'小张',age:18}
{id:2,name:'小李',age:19}
{id:1,name:'小张',age:18}
{id:2,name:'小李',age:19}

初始的虚拟 DOM:

js
<li key={0}>小张---18<input type="text"/></li>
<li key={1}>小李---19<input type="text"/></li>
<li key={0}>小张---18<input type="text"/></li>
<li key={1}>小李---19<input type="text"/></li>

更新后的数据:

js
{id:3,name:'小王',age:20},
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
{id:3,name:'小王',age:20},
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},

更新数据后的虚拟 DOM:

js
<li key={0}>小王---20<input type="text"/></li>
<li key={1}>小张---18<input type="text"/></li>
<li key={2}>小李---19<input type="text"/></li>
<li key={0}>小王---20<input type="text"/></li>
<li key={1}>小张---18<input type="text"/></li>
<li key={2}>小李---19<input type="text"/></li>

原因:当第一次渲染时,子组件 列表的 key 属性被赋值为数组索引, 如果仅仅在尾部插入一个新的组件,前面组件的索引值并不会被变化, 但是,对数据进行了重新排序,数组索引 index 仍然稳定地从 0 开始自增, React 认为组件并没有发生变更,所以在对应数组索引下面的输入框还是在对应的数组索引下面。

慢动作回放----使用 id 唯一标识作为 key

那为什么我们用 id 唯一标识来就不会发现上面的那种情况呢?

又来分析一波:

初始数据:

js
{id:1,name:'小张',age:18}
{id:2,name:'小李',age:19}
{id:1,name:'小张',age:18}
{id:2,name:'小李',age:19}

初始的虚拟 DOM:

js
<li key={1}>小张---18<input type="text"/></li>
<li key={2}>小李---19<input type="text"/></li>
<li key={1}>小张---18<input type="text"/></li>
<li key={2}>小李---19<input type="text"/></li>

更新后的数据:

js
{id:3,name:'小王',age:20},
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},
{id:3,name:'小王',age:20},
{id:1,name:'小张',age:18},
{id:2,name:'小李',age:19},

更新数据后的虚拟 DOM:

js
<li key={3}>小王---20<input type="text"/></li>
<li key={1}>小张---18<input type="text"/></li>
<li key={2}>小李---19<input type="text"/></li>
<li key={3}>小王---20<input type="text"/></li>
<li key={1}>小张---18<input type="text"/></li>
<li key={2}>小李---19<input type="text"/></li>

原因:很简单一句话,key 发生了变化,在 React Diff 算法中认为组件发生变化了,我要从左到右移动元素了。其实这里有个很严重的问题,因为我们是添加一个元素到最前面,对多数据的时候是不利的。

总结

通过例子我们可以看到用 index 作为 key 可能会引发的问题:

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

那我们如何选择 Key 的值呢?

React 官方给出的建议是key 属性的值 应该是字符串类型的唯一标识,通常这个唯一标识来自于 API 返回给前端的数据后端返回的数据中通常都有一个 id 这类的字段,这个字段一般都是全局唯一,并且是稳定地保存在持久层中的,如果是只用来展示用的index也是可以使用的

拓展

额外知识:Vue 和 React 的 Diff 算法比较

相同点:

  • Vue 和 react 的 diff 算法,都是不进行跨层级比较,只做同级比较。

不同点:

  • Vue 进行 diff 时,调用 patch 打补丁函数,一边比较一边给真实的 DOM 打补丁
  • 2.Vue 对比节点,当节点元素类型相同,但是 className 不同时,认为是不同类型的元素,删除重新创建,而 react 则认为是同类型节点,进行修改操作
  • Vue 的列表比对,采用从两端到中间的方式,旧集合和新集合两端各存在两个指针,两两进行比较,如果匹配上了就按照新集合去调整旧集合,每次对比结束后,指针向队列中间移动;
  • 而 react 则是从左往右依次对比,利用元素的 index 和标识 lastIndex 进行比较,如果满足 index < lastIndex 就移动元素,删除和添加则各自按照规则调整;
  • 当一个集合把最后一个节点移动到最前面,react 会把前面的节点依次向后移动,而 Vue 只会把最后一个节点放在最前面,这样的操作来看,Vue 的 diff 性能是高于 react 的