React 中的列表渲染要加 key的原因分析

目录
  • 为什么需要 key?
  • 列表渲染不提供 key 会怎样?
  • 列表渲染的 key 用数组索引会怎样?
  • 应该用什么值作为 key?
  • 结尾

在 React 中我们经常需要渲染列表,比如展示好友列表。

常用写法是用 Arrary.prototype.map 方法,将数组形式的数据映射为 JSX.Element 数组,并嵌入到组件要返回的 JSX.Element 中,如下:

function FriendList() {
  const [items, setItems] = useState(['我们', '小明', '张三']);
  return (
    <ul>
      {items.map((item) => (
        <li key={item}>{item}</li>
      ))}
    </ul>
  );
}

你需要给每个项提供 key 属性作为标识,以区分不同的项。如果你不加 key,React 会警告你:

Warning: Each child in a list should have a unique "key" prop.

为什么需要 key?

在回答这个问题之前,我们先简单了解一下 React 的 DOM Diff 算法原理。

React 会在状态发生变化时,对真实 DOM 树按需批量更新,产生新的 UI。

为此底层做的工作是:将新旧两棵虚拟 DOM 树进行 diff 对比,计算出 patch 补丁,打到真实 DOM 树上。

为了高效,React 的 diff 算法做了限制:

  • 只做同层级的节点对比,不跨层级比较。
  • 如果元素的类型不同(如从 p 变成 div),那它们就是不相同的,会销毁整个旧子树,并调用其下组件的卸载钩子,然后再创建全新的树,相当消耗性能。
  • 如果类型相同,会进行打补丁操作(如更新 className 和标签下的文本内容)。

但这样做会有一个问题,如果同级的多节点  只是位置发生了变化 ,但因为相同索引位置对不上,又发现不能复用,就要销毁一棵树并创建一棵新树,实在是太过于低效了。

于是 React 给开发者提供 key 来标记节点,来优化 React diff 算法,告知 React 某个节点其实没有被移除或不能被原地复用,只是换了位置而已,让 React 更新一下位置。

列表渲染不提供 key 会怎样?

不提供 key,React 就无法确定某个节点是否移动了。

React 就只会对比相同位置的两个节点,如果它们类型相同(比如都是 li 元素),就会对比 props 的不同,进行 props 的打补丁。

​因为 列表渲染通常都是相同的类型,所以位置变动时,多半是会触发节点原地复用效果,倒是不用担心树的销毁重建发生。

原地复用在不提供 key 的时候有时候也是能正确渲染的。

除了一种情况,就是 这个节点有自己的内部状态,最经典的莫过于输入框。

function FriendList() {
  const [items, setItems] = useState(['我们', '小明', '张三']);
  const swap = () => {
    [items[0], items[1]] = [items[1], items[0]];
    setItems([...items]);
  };
  return (
    <div>
      <ul>
        {items.map((item) => (
          <li>{item}<input /></li>
        ))}
      </ul>
      <button onClick={() => { swap(); }}>
        交换
      </button>
    </div>
  );
}

我们给第一和第二个输入框输入内容。

再点击 “交换” 按钮,交换数组第一和第二个元素位置。

然后我们看到 input 前面的文字正确交换了,但是输入框里的内容却没有交换。

​原因是 React 做了原地复用,而 input 没有传 props,不需要打 props 补丁,保持了原样。

这个问题怎么解决?加 key。让 React 知道你的节点需要移动,你得这样写:​

items.map((item) => (
  <li key={item}>{item}<input /></li>
))

不使用 key 的另一个缺点是:因为原地复用会使传入的 props 发生变化,导致不能利用好 React.memo 的组件缓存能力。

列表渲染的 key 用数组索引会怎样?

效果和不使用 key 相同,依旧是新旧节点的相同索引位置对比,但是控制台不会打印警告。

应该用什么值作为 key?

对于节点,你需要用一个唯一的 id 赋值给 key,通常会是数组的 id,比如后端返回的好友列表的好友 id。

const [items, setItems] = useState([
  { id: 5, name: '我们' },
  { id: 9, name: '小明' },
  { id: 87, name: '张三' },
  { id: 91, name: '我们' }
]);
const list = items.map((item) => (
  <li key={item.id}>{item.name}</li>
));

如果后端没有返回 id,你可以自己手动用一个 id 生成器补上一个 id,虽然不太优雅就是了。比如:

const items = ['我们', '张三'];
const genId = (() => {
  let i = 0;
  return () => {
    return i++;
  }
})();
const itemsWithId = items.map(item => ({ id: genId(), val: item }));
// [{id: 0, val: '我们'}, {id: 1, val: '张三'}]

对了,这个 key 只需要在同一个层级的节点唯一即可,不要求所有层级的 key 都是唯一的。

另外,如果你确保你的列表渲染后直到被销毁,不会有位置上的变化,可以使用数组索引为 key。

结尾

对于列表的渲染,我们有必要提供 key,来对节点进行区分,React 的 DOM Diff 算法会基于 key 进行节点位置的调整,确保一些涉及到内部状态的节点的渲染状态。

通常来说,key 值应该是唯一的,通常来自后端返回的数据。在你确认列表不会发生位置变更时,可以使用数组索引作为 key,以去掉恼人的警告提示。

有一个点需要说明的是,key 并不是列表渲染的专属,普通的节点也可以用 key。

到此这篇关于React 中的列表渲染为什么要加 key的文章就介绍到这了,更多相关React列表渲染 key内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 详解React-Native解决键盘遮挡问题(Keyboard遮挡问题)

    本文介绍了React-Native键盘遮挡问题,分享给大家 在开发中经常遇到需要输入的地方,RN给我们提过的TextInput虽然好用,可惜并没有处理遮挡问题. 很多时候键盘弹出来都会遮挡住编辑框,让人很头疼. 本来想在js.coach 库里面找一找第三方的插件,看到最好的一个就是React-native-keyboard-spacer了,然而我们还差一个东西,那就是获取键盘的高度. 这个我也查了半天并没有提供,获取没找到吧.于是只好自己写原生模块去获取键盘的高度了. 关于原生iOS获取键盘高度

  • 详解React中key的作用

    要了解React中key的作用,可以从key的取值入手,key的取值可以分为三种,不定值.索引值.确定且唯一值 在下面的代码中,key的取值是不定值(Math.random()) 问题: 点击按钮的时候,span的颜色会变成红色吗? import React, { useState } from 'react'; function App() { const [initMap, setInitMap] = useState([1,2,3,4]); const handleClick = () =

  • React key值的作用和使用详解

    在react项目中总会遇到这样一个的坑 这是警告数组遍历子元素要有一个唯一的key值,但是key到底是什么,在代码中到底起了什么作用? key概述 react中的key属性,它是一个特殊的属性,它的出现不是给开发者用的(例如你为一个组件设置key之后,也仍无法获取这个组件的key值),而是给react自己用的. 简单来说,react利用key来识别组件,它是一种身份标识标识,就像我们的身份证用来辨识一个人一样.每个key对应一个组件,相同的key react认为是同一个组件,这样后续相同的key

  • react为什么不推荐使用index作为key

    1.旧的虚拟dom和新的虚拟dom对比,首先看他们的key是否相同 2.相同继续对比他们的内容,不同生成新的真实dom进行替换 3.如果内容和key都相同,复用旧的真实dom 不做改变 那么如果我们使用遍历时候自动生成的index作为每个节点的key可能会出现什么问题呢? 下面放个小案例 首先,初始时我们进行遍历persons 他会是这样一个过程,源数据 persons: [ { id: 1, name: "张三", age: 15 }, { id: 2, name: "李四

  • React学习笔记之列表渲染示例详解

    前言 本文主要给大家介绍了关于React列表渲染的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 示例详解: 列表渲染也很简单,利用map方法返回一个新的渲染列表即可,例如: const numbers = [1, 2, 3, 4, 5]; const listItems = numbers.map((number) => <li>{number}</li> ); ReactDOM.render( <ul>{listItems}<

  • React 中的列表渲染要加 key的原因分析

    目录 为什么需要 key? 列表渲染不提供 key 会怎样? 列表渲染的 key 用数组索引会怎样? 应该用什么值作为 key? 结尾 在 React 中我们经常需要渲染列表,比如展示好友列表. 常用写法是用 Arrary.prototype.map 方法,将数组形式的数据映射为 JSX.Element 数组,并嵌入到组件要返回的 JSX.Element 中,如下: function FriendList() { const [items, setItems] = useState(['我们',

  • 浅谈VUE中演示v-for为什么要加key

    说到这个问题想必要举个例子了 没有key <div id="app"> <div> <input type="text" v-model="name"> <button @click="add">添加</button> </div> <ul> <li v-for="(item, i) in list"> <

  • PHP中ID设置自增后不连续的原因分析及解决办法

    PHP中ID设置自增后不连续的原因分析如下所述: alter table tablename drop column id; alter table tablename add id mediumint(8) not null primary key auto_increment first; 每次删除把这两行家伙加上就行了 还有就是这个 使用mysqli对象中的query()方法每次调用只能执行一条SQL命令. 如果需要一次执行多条SQL命令,就必须使用mysqli对象中的 multi_que

  • Vue源码中要const _toStr = Object.prototype.toString的原因分析

    在vue的源码中,vue/src/shared/util.js文件中存放的是一些方法.其中作者用了Object.prototype.toString这个方法来判断类型,但是并没有直接用,而是单独保存在一个变量: const _toStr = Object.prototype.toString 那么为什么要这么做呢? 先说下判断类型.众所周知,typeof在判断对象时不能正确判断Null,并且不能识别出Array,但在判断基础类型时是没问题的.所以尤大也写了: export function is

  • React中Suspense及lazy()懒加载及代码分割原理和使用方式

    目录 React.lazy() 概括 为什么需要懒加载 如何进行代码分割 Suspense Suspense应用场景 Suspense实现原理 总结 Suspense和lazy()都是react中比较新的特性,在项目中使用还比较少,但是学习一下有助于在后面的项目中使用,同样可以一窥React未来的发展方向 React.lazy() 概括 顾名思义lazy()方法是用来对项目代码进行分割,懒加载用的.只有当组件被加载,内部的资源才会导入 为什么需要懒加载 在React的项目中import导入其他组

  • Java类中字段可以不赋予初始值的原因分析

    目录 Java类中字段可以不赋予初始值的原因 下面是在Java类中各字段的初始值 Java中类属性的初始化 连接阶段又可以分为三个子步骤:验证.准备和解析 而我们这里所说的主动使用 包括 初始化一个类包括两个步骤 Java中final变量为什么在使用前必须要进行初始化 Java类中字段可以不赋予初始值的原因 Java虚拟机会对类的实例对象进行分配内存,在分配内存后,会将内存空间(除了对象头)全部初始化为零值.这就保证了,在类的定义过程中,不给字段赋初始值,实例对象也能有初始值. 下面是在Java

  • react中路由和按需加载的问题

    目录 react路由和按需加载问题 1 基本的路由设置 2 如何完成路由的菜单部分 3 如何将每个路由的js文件分开输出 4 react-router按需加载配置 5 最后效果 react路由的基本使用 1.先下包 2.导入并使用 3.使用HashRouter包裹整个应用 4.使用Link指定导航链接 5.使用Route指定路由规则(哪个路径展示哪个组件) 6.精确匹配 :exact 7.Switch 8.处理404页 Redirect react路由和按需加载问题 1 基本的路由设置 reac

  • PHP中header和session_start前不能有输出原因分析

    在http传输文本中,规定必须 header和content顺序必须是:header在前content在后,并且header的格式必须满足"keyword: value\n"这种格式. 1.在header输出之前有输出内容的话,就会造成对header的错误理解(尽管现在已经能容错了),例如不是满足"keyword: value\n"的格式还好,直接错误了,但是满足"keyword: value\n"这个格式以后,客户端是否安装错误理解,还是按照正

  • 详解React中的不可变值

    什么是不可变值 函数式编程是指程序里面的函数和表达式都能像数学中的函数一样,给定了输入值,输出是确定的.比如 let a = 1; let b = a + 1; => a = 1 b = 2; 变量b出现,虽然使用了变量a的值,但是没有修改a的值. 再看我们熟悉的react中的代码,假如初始化了this.state = { count: 1 } componentDidMount() { const newState = { ...state, count: 2 }; // { count: 2

  • JavaScript中的ParseInt("08")和“09”返回0的原因分析及解决办法

    今天在程序中出现一个bugger ,调试了好久,最后才发现,原来是这个问题. 做了一个实验: alert(parseInt("01")),当这个里面的值为01====>07时都是正常的,但是在"08","09"就会返回0 (这种现象出现在ie内核的浏览器中,如360浏览器就会出现这种错误)(谷歌,火狐不受影响) . 查阅资料得知着这种现象原因: 大神的解释: 01--07自然没有问题,但是09,08都是不合格的八进制形式,所以被按照0处理了

随机推荐