React 实践心得:key 属性的原理和用法

我们知道,React 元素可以具有一个特殊的属性 key,这个属性不是给用户自己用的,而是给 React 自己用的。如果我们动态地创建 React 元素,而且 React 元素内包含数量或顺序不确定的子元素时,我们就需要提供 key 这个特殊的属性。

如果你有下面这样的代码:

const UserList = props => (  <div>    <h3>用户列表</h3>    {props.users.map(u => <div>{u.id}:{u.name}</div>)}  // 没有提供 key  </div>);

React 会在控制台打印出报警信息:

Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `App`. See https://fb.me/react-warning-keys for more information.

你必须为数组中的元素提供唯一的 key 属性,就像下面这样:

const UserList = props => (  <div>    <h3>用户列表</h3>    {props.users.map(u => <div key={u.id}>{u.id}:{u.name}</div>)}  // 提供了 key  </div>);

为什么呢?我们知道当组件的属性发生了变化,其 render 方法会被重新调用,组件会被重新渲染。比如 UserList 组件的 users 属性改变了,就得重新渲染 UserList 组件,包括外部的 <div>(容器),内部的一个 <h3> 和若干个 <div>(每一个描述一个用户)。

对后一种 <div>(表示用户的),由于其处在一个长度不确定的数组中,React 需要判断,对数组中的每一项,到底是新建一个元素加入到页面中,还是更新原来的元素。比如以下几种情况:

  • [{name: '张三', age: 20}] => [{name: '张三', age: 21}]:这种情况明显只需要更新元素,没有必要重新创建元素。因为人还是那个人,除了 age,其他信息没有变,显示用户姓名的那个(更小的)元素,是不需要更新(被 ReactDOM 操作到)的。
  • [{name: '张三'}] => [{name: '张三'}, {name: '李四'}] 这种情况,显然需要添加一个新元素来表示李四,这个新元素对应的 DOM 元素会被插入到页面中。
  • [{name: '张三'}] => [{name: '李四'}]:这种情况就有点复杂了,似乎两种方案都可以。可以把表示张三的元素删掉,为李四新建一个,当然是非常合理的选择。但是直接把张三的元素换成李四,似乎也无不可。

实际上,如果真的认为上述第3种的后一种方案也无不可,那可是大错特错了。为什么呢:

  • 考虑这种情况:[{name: '张三'}, {name: '李四'}] => [{name: '李四'}, {name: '张三'}],难道也需要把张三的元素更新成李四的,李四的元素更新成张三的吗?

那么,为数组中的元素传一个唯一的 key(比如用户的 ID),就很好地解决了这个问题。React 比较更新前后的元素 key 值,如果相同则更新,如果不同则销毁之前的,重新创建一个元素。

那么,为什么只有数组中的元素需要有唯一的 key,而其他的元素(比如上面的<h3>用户列表</h3>)则不需要呢?答案是:React 有能力辨别出,更新前后元素的对应关系。这一点,也许直接看 JSX 不够明显,看 Babel 转换后的 React.createElement 则清晰很多:

// 转换前const element = (  <div>    <h3>example</h3>    {[<p key={1}>hello</p>, <p key={2}>world</p>]}  </div>);

// 转换后"use strict";

var element = React.createElement(  "div",  null,  React.createElement("h3",null,"example"),  [    React.createElement("p",{ key: 1 },"hello"),     React.createElement("p",{ key: 2 },"world")  ]);

不管 props 如何变化,数组外的每个元素始终出现在 React.createElement() 参数列表中的固定位置,这个位置就是天然的 key。

题外话

初学 React 时还容易产生另一个困惑,那就是为什么 JSX 不支持 if 表达式来有选择地输出(不能这样:{if(yes){ <div {...props}/> }}),而必须采用三元运算符来完成这项工作(必须这样:{yes ? <div {...props}/>} : null)。那是因为,React 需要一个 null 去占住那个元素本来的位置。

曾经,我天真的以为 key 这个元素只应在数组中使用。直到我在一个复杂的项目中写出了及其恶心的 componentWillReceiveProps方法。我尝试寻找销毁和重建组件,触发componentDidMount 方法,重置 state,然后才突然发现 key 这个属性已经在那里了。

举个例子,我们有一个展示用户信息的 UserDashboard 组件。传给组件的 props 只有用户的 基本信息(ID,姓名等),而有关用户的详细信息(比如当前是否在线等)是需要请求过来的。组件内的一些操作(比如尝试与该用户聊天)也会使用请求,组件本身也有各种状态(比如是否显示聊天框)。

整个界面上最多只有一个 UserDashboard,但某些操作(比如点击旁边的 UserList)可能会切换 UserDashboard 的目标用户,那么问题就来了:当目标用户切换的时候,UserDashboard仅仅是一个普通的更新操作,触发的是componentWillReceivePropsshouldComponentUpdatecomponentWillUpdatecomponentDidUpdate这一套方法。我们需要在 componentWillReceiveProps 中做太多的事情:检测这次 props 的更新是否改变了用户的 ID,如果是的话,我们需要检查 UserDashboard 发出去的请求是否都得到了响应,对还未收到响应的请求注销其响应函数(否则上一个用户的在线状态有可能显示在这一个用户上);我们还要更新 UserDashboard 上的几乎所有状态(切换用户的时候总要把聊天框关闭吧);如果我们还不幸地用的 ref 做了一些神奇的 hack,那么你还要去手动把之前做的事情复原回来,这简直要成一团乱麻了!当 UserDashborad 的逻辑,你的componentWillReceiveProps 方法里会充斥着晦涩难懂的只有你能看懂的代码(两周后你自己也看不懂了)。

解决方案是什么?就是用 key 属性。在 JSX 中使用 UserDashboard 的时候,不仅把userInfo 传入,把 userInfo.id 作为名为 key 的 props 传入(尽管 UserDashboard 不是数组中的组件)。这样切换目标用户的时候,key 属性也变了,React 会自动销毁之前的组件,用一个全新的组件来渲染新的用户:我们可以从容地在 componentWillUnmount 里作清理工作(注销请求的响应函数,防止其更新一个 unmounted component),至于重置state 这些工作已经不需要做了,由于组件不再是更新,而是销毁和重建,已经是天然完成的。

当然,你可以质疑这样做是否会影响性能。我认为,只要目标用户的切换不够频繁,对性能的影响是很小的。如果不使用 key 触发组件的销毁和重建,任由组件自行「更新」,每次切换时更新的内容也是很多的。这时,我们使用 key 带来的性能损耗是完全可以接受的,而带来的收益却非常大。

所以,我想说的结论是:为了组件内部逻辑的清晰,你几乎应该在任何复杂的有状态组件(尤其是有具体对应对象的)上使用key属性(只要 key 属性的改变不是很频繁),这样做,才能在合适的时候触发组件的销毁与重建,组件才能有一个健康的生命周期。

题外话

配合 react-router 时,通常要为 route 组件赋 key,但通常情况下我们是没法传 props 给 route 组件的。解决的方案是 createElement 方法,如下所示。

class App extends Component {  static createElement = (Component, ownProps) => {    const {userId} = ownProps.params;    switch (Component) {      case UserDashboard:        return <Component key={userId} {...ownProps}/>;      default:        return <Component {...ownProps}/>;    }  };  render() {    return (      <Provider store={store}>        <Router createElement={App.createElement}                 history={syncHistoryWithStore(hashHistory, store)}>          <Route path="/" component={Home}>            <IndexRoute component={Index}/>            <Route path="users/:userId" component={UserDashboard}/>          </Route>        </Router>      </Provider>    )  }}

转载自:http://taobaofed.org/blog/2016/08/24/react-key/作者: 叶斋

时间: 2025-01-19 03:36:54

React 实践心得:key 属性的原理和用法的相关文章

React 实践心得:react-redux 之 connect 方法详解

Redux 是「React 全家桶」中极为重要的一员,它试图为 React 应用提供「可预测化的状态管理」机制.Redux 本身足够简单,除了 React,它还能够支持其他界面框架.所以如果要将 Redux 和 React 结合起来使用,就还需要一些额外的工具,其中最重要的莫过于 react-redux 了. react-redux 提供了两个重要的对象,Provider 和 connect,前者使 React 组件可被连接(connectable),后者把 React 组件和 Redux 的

Key 属性

  在 Dictionary 对象中设置一个 key. object.Key(key) = newkey Key 属性包括下面这些部分: 部分 描述 object 必选项.总是一个 Dictionary 对象的名称. key 必选项.被改变的 key 值. newkey 必选项.替换所指定的 key 的新值. 说明 如果在改变一个 key 时没有发现该 key,那么将创建一个新的 key 并且其相关联的 item 被设置为空. 下面这段代码说明了 Key 属性的用法: var d;d = new

[原创]XSL学习心得 - 调用属性值

 [原创]XSL学习心得 - 调用属性值也许这个问题很简单吧,但是刚才查了好多篇入门教程都没有涉及,幸好hsower发的xsl语法介绍中有详细的说明.    <xsl:value-of select="元素名称/@属性名称"/>    比如一段XML代码是这样的:   <王朔 网址="www.wangshuo.com">知名作家加著名评论家王朔先生的地方</王朔>    我们可以用<xsl:value-of select=&

VBS教程:属性-Key 属性_vbs

Key 属性 在 Dictionary 对象中设置 key. object.Key(key) = newkey 参数 object 必选项.通常是 Dictionary 对象的名称. Key 必选项.要改变的 Key 值. Newkey 必选项.代替指定 key 值的新值. 说明 如果在更改 key 值时未找到 key,则出现运行时错误. 下面例子举例说明如何使用 Key 属性: Function DicDemo Dim d '创建变量. Set d = CreateObject("Script

Android 属性动画原理与DataBinding

Android 属性动画原理与DataBinding 看到这个标题的时候你可能会有疑问,属性动画和 DataBinding 之间有什么关系?我个人理解的是:它们内部的实现思想有相似之处.这篇文章主要对 Android 属性动画的知识通过文字进行整理记录,内容参考于<Android开发艺术探索>,在最后会给出我如此理解属性动画和 DataBinding 的原因. Android动画概述: Android 的动画可以分为三种:View 动画.帧动画和属性动画,View 动画通过对场景里的对象不断做

go语言的sql包原理与用法分析_Golang

本文实例讲述了go语言的sql包原理与用法.分享给大家供大家参考,具体如下: go的sql包是在pkg/database中,里面的两个包sql和sql/driver可以一起看.建议看这个两个包之前可以先看看sql文件夹下的doc.txt.这个文档说了几点比较重要的: 1 这两个包是真正Go风格的包. 2 这使用这两个包就不需要关于并发处理了,也不需要维护自己的数据库连接池了,一旦建立了一个连接,这个连接是可以在各个goroutine之间共用的. 3 sql/driver提供的是数据库的接口,具体

Python Property属性的2种用法

  这篇文章主要介绍了Python Property属性的2种用法,本文分别给出了两种用法的代码实例,需要的朋友可以参考下 假设定义了一个类:C,该类必须继承自object类,有一私有变量_x 代码如下: class C: def __init__(self): self.__x=None 1.现在介绍第一种使用属性的方法: 在该类中定义三个函数,分别用作赋值.取值和删除变量(此处表达也许不很清晰,请看示例) def getx(self): return self.__x def setx(se

jQuery根据name属性进行查找的用法分析_jquery

本文实例讲述了jQuery根据name属性进行查找的用法.分享给大家供大家参考,具体如下: $("div[id]") //选择所有含有id属性的div元素 $("input[name='newsletter']") //选择所有的name属性等于'newsletter'的input元素 $("input[name!='newsletter']") //选择所有的name属性不等于'newsletter'的input元素 $("input

Yii中srbac权限扩展模块工作原理与用法分析_php实例

本文实例讲述了Yii中srbac权限扩展模块工作原理与用法.分享给大家供大家参考,具体如下: 1. 设置权限规则表:可放在module模块配置文件里面 public function init() { //操作权限表,必须存在以下字段: //itemname角色名/ID, //type授权项目类型/1(任务)或者2(角色), //bizrule权限/逻辑运算表达式为false是有权限操作, //data数据/YII暂无利用 Yii::app()->authManager->itemTable