9

八、Lists和Keys的处理

首先,让我们回顾一下如何在JavaScript中遍历lists。

下面的代码,我们使用map()函数获取一个数字数组,并将它们的值加倍。 我们将map()返回的新数组赋给变量doubled并记录下来:

const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(item => item * 2);
console.log(doubled); //=> [2, 4, 6, 8, 10]

上面的代码会在控制台打印[2, 4, 6, 8, 10]
在React中,将数组转换为元素集合几乎和上面的代码是一样的。

渲染多个组件

您可以自己一个创建元素集合,并使用花括号{}将它们包含在JSX中。
下面,我们使用Javascript map()函数循环一个数字数组。 我们为每个item返回一个<li>元素。 最后,我们将结果数组的元素赋给listItems

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map(item => <li>{item}</li>);

我们将整个listItems数组包含在<ul>元素中,并将其渲染到DOM:

ReactDOM.render(
    <ul>{listItems}</ul>,
    document.getElementById('root')
)

此代码将会显示1到5之间的数字。

基础的List组件

通常你会将List放在组件中。
我们可以将前面的例子重构为接受数字数组的组件,并输出一个无序的元素列表。

import React from 'react';
import ReactDOM from 'react-dom';

function NumberList(props) {
    const numbers = props.numbers;
    const listItems = numbers.map(item => <li>{item}</li>);

    return (
        <ul>{listItems}</ul>
    );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
    <NumberList numbers={numbers} />,
    document.getElementById('root')
);

当您运行此代码时,将会收到一条警告,Each child in an array or iterator should have a unique "key" prop. Check the render method of "NumberList".

提示指出应该为列表的每一项提供一个属性key“key”是创建元素列表时需要包含的特殊字符串属性。 我们将在下一节讨论为什么它很重要。

让我们给numbers.map()的列表项分配一个key,并修复那个提示缺少key的问题。

import React from 'react';
import ReactDOM from 'react-dom';

function NumberList(props) {
    const numbers = props.numbers;
-    const listItems = numbers.map(item => <li>{item}</li>);
+    const listItems = numbers.map(item => <li key={item.toString()}>{item}</li>);

    return (
        <ul>{listItems}</ul>
    );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
    <NumberList numbers={numbers}/>,
    document.getElementById('root')
)

Key的用法

keys帮助确定哪些item已更改,已添加或已删除。 应该给数组中的元素设置上key属性,以便给元素一个稳定的身份:

const numbers =[1, 2, 3, 4, 5];
const listItems = numbers.map(item => 
    <li key={item.toString()}>
        {item}
    </li>
);

最好的方法是使用一个字符串来选择key,它是其兄弟之间一个列表项的唯一标识。 通常,您会使用数据中的ID作为key:

const todoItems = todos.map(todo =>
    <li key={todo.id}>
        {todo.text}
    </li>
);

当您对已渲染的item没有稳定的ID时,您可以将项目index用作关键字:

const todoItems = todos.map((todo, index) =>
    // 如果todo没有一个稳定的id,可以使用这种方法
    <li key={index}>
        {todo}
    </li>
);

如果项目需要实现重新排序,我们不建议为key使用索引,因为这将很慢。

合理提取组件中的Keys

Keys仅在循环时的上下文中有意义。

例如,如果您提取了一个ListItem组件,则应该将该key保存在数组中的<ListItem />元素上,而不是ListItem本身的根<li>元素上。

示例:key的错误用法

function ListItem(props) {
    const value = props.value;
    return (
        <li key={value.toString()}>
            {value}
        </li>
    );
}

function NumberList(props) {
    const numbers = props.numbers;
    const listItems = numbers.map(item =>
        // 这是错误的,这里应该设置上key
        <ListItem value={item} />
    );
    return (
        <ul>{listItems}</ul>
    );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
    <NumberList numbers={numbers}> />,
    document.getElementById('root')
)

实例:key的正确用法

function ListItem(props) {
    // 这才是正确的,在这里不需要设置key
    return <li>{props.value}</li>;
}

function NumberList(props) {
    const numbers = props.numbers;
    const listItems = numbers.map(item => 
        // 这才是正确的,在这里设置key
        <ListItem key={item.toString()} value={item} />
    );
    return (
        <ul>
            {listItems}
        </ul>
    );
}

const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
    <NumberList numbers={numbers} />,
    document.getElementById('root')
)

注意:map()中的元素都需要属性key。在哪儿循环就在哪儿设置key。

key在兄弟组件中必须是唯一的

数组中使用的key在其兄弟组件之间应该是唯一的。 但是,它们不需要是全局唯一的。 当我们生成两个不同的数组时,我们可以使用相同的键:

function Blog(props) {
   const sidebar = (
       <ul>
           {props.posts.map(post => 
               <li key={post.id}>
                   {post.title}
               </li>
           )}
       </ul>
   );
   
   const content = props.posts.map(post =>
      <div key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.content}</p>
      </div>
   );
   
   return (
       <div>
           {sidebar}
           <hr />
           {content}
       </div>
   );
}

const posts = [
    {id: 1, title: 'hello zhangyatao', content: 'you\'re so handsome!'},
    {id: 2, title: 'hi jiangyanyun', content: 'you\'re so beautiful!'}
];

ReactDOM.render(
    <Blog posts={posts} />,
    document.getElementById('root')
)

key用来作为React的观察点,但它们不会传递给您的组件。 如果你需要在组件中使用相同的值,则使用不同的名称显式地将它作为props传递:

const content = posts.map(post =>
    <Post key={post.id} id={post.id} title={post.title} />
);

使用上面的示例,Post组件可以读取props.id,但不能读取props.key

在JSX中嵌入map()

在上面的示例中,我们声明了一个单独的listItems变量并将其包含在JSX中:

function NumberList(props) {
   const numbers = props.numbers;
   const listItems = numbers.map(item =>
       <ListItem key={item.toString()} value={item} />
   );
   return (
       <ul>{listItems}</ul>
   );
}

JSX允许在大括号中嵌入任何表达式,所以我们可以内联map()结果:

function NumberList(props) {
    const numbers = props.numbers;
    return (
        <ul>
            {numbers.map(item =>
                <ListItem key={item.toString()} value={item} />
            )}
        </ul>
    );
}

这会让我们的代码更清晰,这种风格可以被随意使用。
就像在JavaScript中,它是由你决定是否值得提取一个变量的可读性。
请记住,如果map()主体嵌套太多层,那么它是抽出一个组件的好时机。


张亚涛
5.3k 声望2.8k 粉丝

人首先应该接受现实,承认问题的存在,并且反思它。而不是首先就跳出来回避这个问题,找比我们更差的。质疑甚至抨击提出问题的人,并且一杆子打死。