React中列表渲染
在上一节中,我们学习了如何在React中实现条件渲染。今天我们一起来学习如何在React中实现列表渲染(在Vue中我们可以使用v-for
指令)。因为在实际开发中我们时常要处理一些列表的渲染,比如在《列表渲染和Vue的v-for
指令》一文中示例:
上图中我们有多个列表的渲染,比如Tweets列表,关注用户列表等。
在React中,处理列表渲染和处理条件渲染类似,需要借助JavaScript的一些原生能力来辅助我们实现列表渲染。如果你对这方面知识感兴趣的话,欢迎继续往下阅读。
回顾JavaScript中的循环
在开始学习React列表渲染之前,我们很有必要先简单的回顾JavaScript中有关于循环相关的知识。原因很简单:
React中的列表渲染用的就是JavaScript原生的循环知识。
简单地说,循环就是重复做一件事件。在MDN中有一张图形象的阐述了这个概念:
上图是位农夫为他的家庭准备一周的食物计划。为了完成这个计划,他或许需要执行一个循环。一个循环通常会需要一些特定的条件:
- 一个开始条件:这是循环的起点(比如上图中的“没有食材了”)。用到代码中,它被初始化为一个特定的值(常称初始条件)
- 一个结束条件:这是循环的结束点(比如上图中的“有足够的食材吗?”)。用到代码中,它就是循环的停止标准(常称结束条件),通常计数器达到一定的值(假设,该农夫整个家庭一周有十种食物就可以满足)
- 一个迭代器:这通常在每个连续循环上递增(或递减)一个计数器,直到达到结束条件。比如,农夫能够每小时收集到两份食物,每小时后,他收集的食物增量就增加了两倍,他检查他是否有足够的食物。如果达到了十份食物(退出条件),该农夫就可以停止收集食物
如果我们用伪代码来描述上图中农夫准备食物这样的场景,可能会像下面这样:
loop(food=0; foodNeeded=10){
// 农夫目前的食物为0 ~> 初始条件 food=0
// 农夫一周需要的食物定量是10 ~> 结束条件 fooNeeded=10
// 如果农夫当前食物和所需食物数量相等 ~> food = foodNeeded
if (food = foodNeeded) {
exit loop; // ~> 退出循环
// 农夫有足够的食物了,停止收集食物
} else {
food +=2; // 每一个小时,农夫可以收集到两件食物 ~> 循环迭代计数器 (递增或递减)
// 循环将继续执行
}
}
如果我们用JavaScript来实现上述这样的场景,就可以使用循环语句来处理。关键是JavaScript中循环有很多种类型,但它们的本质是做同样的一件事情:
把一个动作重复了很多次(也有可能重复的次数是
0
)。
JavaScript中各种循环机制提供了不同的方法去确定循环的开始和结束。不同情况下,某一种类型的循环会比其他的循环用起来更简单。
在JavaScript中,最基础的、最原始的循环语句是for
语句、do...while
语句和while
语句。如果用图来描述的话,大致如下:
while
语句
while
语句只要指定**条件求值为真(true
)**就会一直执行它的语句块。比如下面这样的示例:
var n = 0;
var x = 0;
while (n < 3) {
n++;
x += n;
console.log(x)
}
在每次循环里,n
会增加1
,并被加到x
上。所以,x
和n
的变化是:
- 第一次完成后:
n=1
,x=1
- 第二次完成后:
n=2
,x=3
- 第三次完成后:
n=3
,x=6
在三次完成后,条件n<3
的结果不再为真,所以循环终止了。
for
语句
for
循环会一直重复执行,直到指定的循环条件为false
。当一个for
循环执行的时候,会发生以下过程:
- 如果有初始化表达式
initialExpression
,它将被执行。这个表达式通常会初始化一个或多个循环计数器,但语法上是允许一个任意复杂度的表达式的。这个表达式可以声明变量 - 计算
condition
表达式的值。如果condition
的值是true
,循环中的语句会被执行。如果condition
的值是false
,for
循环终止。如果condition
表达式整个都被省略掉了,condition
的值会被认为是true
- 循环中的
statement
被执行。如果需要执行多条语句,可以使用块({...}
)来包裹这些语句 - 如果有更新表达式
incrementExpression
,执行更新表达式 - 回到第2步,重新执行
比如下面这样的小示例:
for (i = 0, len = 3; i < len; i++) {
console.log(i)
}
整个for
循环执行过程是这样:
- 第一次完成后,
i=0
- 第二次完成后,
i=1
- 第三次完成后,
i=2
当i<len
(即可i<3
)时,条件不再为真,循环会终止。
do...while
语句
do...while
语句一直重复直到指定的条件求值得到假值(false
)。代码声明块(statement
)在检查条件之前会执行一次(至少会执行一次)。要执行多条件语句(语句块),要使用块语句({...}
)包裹起来。如果条件(condition
)为值(true
),statement
将再次执行。在每个执行的结束会进行条件的检查。当condition
为假(false
),执行会停止并且把控制权交回给do...while
后面的语句。
比如下面的小示例:
var i = 0;
do {
i += 1;
console.log(i);
} while (i < 3);
这个do...while
循环将至少重复一次,并且一直重复直到i
不再小于3
,循环停止。
这些是JavaScript中最基础的知识,如果你对这方面感兴趣的话,建议花点时间阅读下面几篇文章:
- JavaScript中的所有循环类型
- MDN:循环吧代码
- MDN:循环与迭代
- For, While, and Do...While Loops in JavaScript
- The Complete Guide To Loops
其他迭代和遍历的方法
而我们在处理数据(根据数据渲染列表)一般都是对数组或对象这样的JSON
数据做遍历处理。比如我们要渲染下图中“你可能会喜欢”的列表区块:
服务端可能提供的JSON
数据会是像下面这样:
const followData = [
{
id: '940202913393082373',
avatar: 'https://pbs.twimg.com/profile_images/940202913393082373/23X8DJQc_x96.jpg',
nick: 'Bloomberg TicToc',
account: '@tictoc',
isFollowed: true,
certification: true,
recommended: true
},
{
id: '1021778918842531840',
avatar: 'https://pbs.twimg.com/profile_images/1021778918842531840/FBVP_uDf_x96.jpg',
nick: 'Netlify',
account: '@Netlify',
isFollowed: false,
certification: false,
recommended: false
},
{
id: '1070861268246978560',
avatar: 'https://pbs.twimg.com/profile_images/1070861268246978560/ObS2CG3t_x96.jpg',
nick: 'npm,Inc.',
account: '@npmjs',
isFollowed: true,
certification: true,
recommended: false
}
]
也就是说,我们需要对上面的数组(当然,有的时候也有可能是一个对象)进行遍历。但是在JavaScript中,针对数组和对象进行遍历采用的方法也有所不同,有关于这方面的介绍可以阅读:
- 数组迭代方法
- JavaScript数组的那些事儿
- JavaScript数组所有API全解密
- 如何遍历JavaScript中对象属性
- 对象属性的枚举
- DOM树和遍历DOM
- Deep Dive into JavaScript's Array Map Method
- JAVASCRIPT WITHOUT LOOPS
- Iterables and iterators in ECMAScript 6
- Why Object Literals in JavaScript Are Cool
- What you should know about JavaScript arrays
如果你了解JavaScript中数组或对象遍历方法或者阅读了上面相关的文章,你会发现在现代的JavaScript中有更多的了遍历方法。比如:forEach()
与map()
,every()
与some()
,filter()
、find()
与findIndex()
,keys()
、values()
与entries()
,reduce()
与reduceRight()
等。其中有些是ES5中的,也有一些是ES6中的方法;有些是用于数组的遍历,也有一些是用于对象的遍历。
事实上,不管是处理一个数组还是一个对象,我们想要的是拿到相应的字段。很多时候我们可以将Object
和Array
的一些方法结合起来使用达到自己所需要的目的。比如下面这样的小示例:
const profile = {
id: '1021778918842531840',
avatar: 'https://pbs.twimg.com/profile_images/1021778918842531840/FBVP_uDf_x96.jpg',
nick: 'Netlify',
account: '@Netlify',
isFollowed: false,
certification: false,
recommended: false
}
通过Object
的.keys()
和.values()
可以拿到对象的key
和value
,并且输出的是相应的数组:
如果配合Array
相应的遍历方法,就拿forEach()
来说,可以将值一个一个输出,比如:
虽然在JavaScript中用于遍历数据集的各种技术和方法有很多,但比较流行的方法之一是.map()
方法。而且.map()
方法是一个非变异的方法,因为它创建了一个新的数组,而不是只改变调用数组的方法。在React的列表渲染中也经常会用到.map()
方法。这里简单的来看看.map()
方法的使用。
JavaScript的.map()
方法接受一个回调函数作为其参数之一,该函数的一个重要参数是该函数正在处理的项的当前值,这也是一个必要的参数。比如上面的所列的followData
数组,如果我们想取出该数组中每一项(每一项是一个Object)的account
字段,我们可以这样写:
const getAccount = followData.map((item) => {
console.log(item.account)
})
如果把
.map()
取到的每项值丢到DOM标签内,就可以生成我们想要的列表,稍后会用到。
.map()
方法除了可以处理数组之外,还可以用于迭代数组中的对象。同样拿followData
数组来举例:
const followInfo = followData.map(item =>{
const nickObj = {}
nickObj[item.account] = item.nick
console.log(nickObj)
})
有关于.map()
更多的介绍,还可以阅读下面相关文章:
- When to Use Map instead of Plain JavaScript Object
- Understanding JavaScript's map()
- Functional Programming JavaScript Map() Function Demystified
- 4 Uses of JavaScript's Array.map() You Should Know
React列表渲染
前面提到过,React中的列表渲染和其他的一些流行框架(比如Vue)做法有些不同。比如在Vue框架中有自已的指令v-for
可以在HTML中直接迭代,但在React中,只能使用原生JavaScript方法来进行迭代。接下来我们从不同的角度来了解和学习React的列表渲染相关的知识点。
创建一个简单地静态列表
我们先从一个简单地列表开始,比如像下面这样的一个菜单列表:
如果你和我一样是位React的初学者的话,要在React中构建这样的菜单列表,可能想到的就是一个静态列表。可能会把一坨HTML的代码和JSX结合起来:
const Menu = () => {
return (
<ul className="nav">
<li><a href="#">主页</a></li>
<li><a href="#">探索</a></li>
<li><a href="#">通知</a></li>
<li><a href="#">私信</a></li>
<li><a href="#">书签</a></li>
<li><a href="#">列表</a></li>
<li><a href="#">个人资料</a></li>
<li><a href="#">更多</a></li>
</ul>
)
}
添加一些样式的话,看到的效果大致如下:
动态创建列表
上面的方式虽然能渲染出列表,但这样除了没有用好React框架的优势之外还让代码难于维护。前面我们也提到过,在React中我们可以使用JavaScript的循环迭代能力,轻松的实现列表的渲染。另外我们拿到的数据一般都会是一个对象或数组。比如上面的菜单列表,如果我们把所有的菜单项放到一起就是可以是一个数组,比如:
const menuData = ['主页', '探索', '通知', '私信', '书签', '列表', '个人资料', '更多']
这样我们就可以通过.map()
方法来简化上面的静态列表的代码:
const Menu = () => {
return (
<ul className="nav">
{menuData.map(menu => (
<li><a href="#">{menu}</a></li>
))}
</ul>
)
}
得到的效果是一样的:
事实上,上面的示例仅仅是在静态列表上显得更优雅一点,但如果Menu
组件要运用到另一个组件中(Menu
组件中的menuData
有可能会随着组件的运用场景而变化),其局限性就显现出来了。不过不用着急,我们可以借用props
来对上面的组件做一个改造:
const menuData = ['主页', '探索', '通知', '私信', '书签', '列表', '个人资料', '更多']
const Menu = ({lists}) => {
const listsItem = lists.map(item => <li><a href="#">{item}</a></li>)
return (
<ul className="nav">{listsItem}</ul>
)
}
const rootElement = document.getElementById("app");
ReactDOM.render(<Menu lists={menuData} />, rootElement);
值得注意的是,在React中渲染列表时如果未显式的给每个列表项设置key
属性时,在浏览器上会有一个警告信息:
有关于React列表渲染时为什么要显式给列表项设置
key
属性更详细的介绍,我们稍后会做相关的讨论。
如何在React中渲染对象列表
前面的示例我们看到的数据源都是一个数组,但很多时候我们这个数组是一个对象的集合。比如前面提到的followData
,它就是一个含有对象的数组集合。在React中对于JavaScript数组中的复杂对象,和前面我们看到的示例没有什么差异,同样可以使用.map()
方法来渲染列表。比如下面这个示例:
const Follow = ({lists}) => {
const listsItem = lists.map(item => (
<div className="media" key={item.id}>
<div className="media__object">
<img src={item.avatar} alt={item.nick} />
</div>
<div className="media__body">
<h5>{item.nick} {item.certification && <span className="certification">认证</span>}</h5>
<div className="account">{item.account}</div>
{item.recommended && <div className="recommended">推荐</div>}
</div>
{!item.isFollowed && <button className="button">关注</button>}
</div>
))
return <div className="card">{listsItem}</div>
}
const rootElement = document.getElementById("app");
ReactDOM.render(<Follow lists={followData} />, rootElement);
效果如下:
该示例实现的迭代遍历方法和前面的没有太大的差异,使用的还是.map()
方法。其实JavaScript中其他迭代遍历方式方法都有效,只不过.map()
在React中列表渲染使用的是最多的方法。另外上面的示例中,如果你足够仔细的话,我们在做一些条件判断的时候,运用到了上篇文章中介绍到的相关方案。这样做主要目的之一是让代码足够简洁,这也符合了我们前面有关于JSX方面中学习到的知识,即编写出更为优雅的JSX代码。
React中嵌套列表渲染
上面示例看到的效果都是单维的列表渲染,有的时候我们会需要处理一些嵌套列表的渲染(比如二维列表)。在React中渲染嵌套列表仍然可以使用以前的实现技术。比如我们实现一个下拉菜单的,该菜单的数据可能是这样:
const listsData = [
{
title: 'Dashboard',
items: ['Tools', 'Reports', 'Analytics', 'Code Blocks']
},
{
title: 'Sales',
items: ['New Sales', 'Expired Sales', 'Sales Reports', 'Deliveries']
},
{
title: ' Messages',
items: ['Inbox', 'Outbox', 'Sent', 'Archived']
}
]
同样使用.map()
方法来遍历,只不过为了实现二维列表的渲染,可能会多次采用.map()
方法,比如:
const Menu = ({lists}) => {
return (
<ul className="nav">
{
lists.map((list, index) => (
<li key={index}>
<input id={list.title} name="menu" type="radio" />
<label htmlFor={list.title}>
<span className="nav__title">{list.title}</span>
<ul className="nav__sub">
{
list.items.map((item, index) => (
<li key={index}>{item}</li>
))
}
</ul>
</label>
</li>
))
}
</ul>
)
}
const rootElement = document.getElementById("app");
ReactDOM.render(<Menu lists={listsData} />, rootElement);
效果如下:
只不过这样做,会让应用程序变得更复杂,可维护性的负担也会变大。这就是为什么在《如何编写出优雅的JSX》建议我们尽可能的把逻辑抽得简单化原因之一。
如何在React中处理重复元素中的children
在React中构建组件时,同样也会涉及到父子组件的关系。在渲染重复元素时我们有什么方式能更好的处理子组件呢?接下来我们用一个时钟组件Clock
来举例,和大家一起探讨和学习这方面的知识。
假设我们想要构建的Clock
组件,可以灵活的设置时钟的样式(24
小时时钟或12
小时时钟),不同的分隔符,任意自己想要的时钟参数(小时,分钟,秒)。为了有一个更具灵活性的Clock
组件,我们可以将其分解成多个组件,比如Hour
(小时)、Minute
(分钟)、Second
(秒)、Separator
(分隔符)和Ampm
等。
在React中创建组件有多种不同的方式,在这个示例中,我们采用的是无状态函数组件(Function Stateless)。
这些组件都只是函数,写起来不会很复杂,大致可能像下面这样:
const Hour = (props) => {
let {hours} = props
if (props.twelveHours) {
if(hours <= 12){
hours = hours
}else if(hours > 12 && hours < 24){
hours -= 12
}else if(hours == 24){
hours = '00'
}
}
return <span>{hours}</span>
}
const Minute = (props) => {
const {minutes} = props
return <span>{minutes < 10 && '0'}{minutes}</span>
}
const Second = (props) => {
const {seconds} = props
return <span>{seconds < 10 && '0'}{seconds}</span>
}
const Separator = (props) => {
const {separator} = props
return <span>{separator || ':'}</span>
}
const Ampm = (props) => {
const {hours} = props
return <span>{ hours >= 12 ? 'pm' : 'am'}</span>
}
在些基础上,还需要另一个组件,让Clock
组件可以接受一个format
字符串并分解这个字符串。这样做的目的是让用户能更灵活的配置自己想要的时钟方式。比如这里,我们新创建一个叫Formatter
的组件,其代码可能会像下面这样:
const Formatter = (props) => {
const {format} = props
let children = format.split('').map((e, idx) => {
switch(e) {
case 'h':
return <Hour key={idx} {...props} />
case 'm':
return <Minute key={idx} {...props} />
case 's':
return <Second key={idx} {...props} />
case 'p':
return <Ampm key={idx} {...props} />
case ' ':
return <span key={idx}> </span>
default:
return <Separator key={idx} {...props} />
}
})
return <div className="timer">{children}</div>
}
const hour = new Date().getHours();
const minute = new Date().getMinutes();
const second = new Date().getSeconds();
const rootElement = document.getElementById("app");
ReactDOM.render(<Formatter format="h:m:s" hours={hour} minutes={minute} seconds={second} />, rootElement);
这个时候你可以看到时间的输出。
上图看到是按照用户所需要的格式format="h:m:s"
方式输出的格式化好的时间。虽然达到我们所需要的效果,但上面示例中的key
和props
的使用的不够优雅。其实我们可以借助React的一些其他特性,使用React.Children
对象映射
到React
对象的列表上。这样一来,我们可以将Formatter
的代码进行优化:
const Formatter = (props) => {
const {format} = props
let children = format.split('').map(e => {
switch(e) {
case 'h':
return <Hour />
case 'm':
return <Minute />
case 's':
return <Second />
case 'p':
return <Ampm />
case ' ':
return <span> </span>
default:
return <Separator />
}
})
return <div className="timer">{React.Children.map(children, c => React.cloneElement(c, props))}</div>
}
const currentTime = new Date();
const hour = currentTime.getHours();
const minute = currentTime.getMinutes();
const second = currentTime.getSeconds();
const rootElement = document.getElementById("app");
ReactDOM.render(<Formatter format="h:m:s" hours={hour} minutes={minute} seconds={second} />, rootElement);
最终得到的效果是一样的:
这里用到了React.cloneElement
和React.Children.map
相关的知识,这里不做过多阐述,感兴趣的同学可以阅读下面相关的文章:
- How to pass data to props.children
- Creating flexible components
- Transforming Elements In React
- Building React Components Using Children Props and Context API
- Three Ways to use Hooks to Build Better React Components
- Compound Components in React: Using the Context API
上面示例看到的只是如何格式化时间格式,但时钟并不会动,接下来构建Clock
组件,让其动起来:
const Clock = () => {
const [currentTime, setCurrentTime] = React.useState(new Date())
React.useEffect(()=>{
const timer = setTimeout(()=>{
setCurrentTime(new Date())
}, 1000)
retrun () => {
clearTimeout(timer)
}
}, [currentTime])
const hour = currentTime.getHours()
const minute = currentTime.getMinutes()
const second = currentTime.getSeconds()
return <Formatter format="h:m:s" hours={hour} minutes={minute} seconds={second} />
}
效果如下:
有关于React中
children
更深入的探讨和学习,我们在接下来的学习中会和大家一起讨论,感兴趣的同学,欢迎持续关注相关的更新。
React中列表渲染中的key
前面提到过,React中列表渲染的时候,如果没有给列表项显式设置key
属性是会报错的。在React中key
可以帮助React识别哪些元素改变了,比如被添加或删除,另外使用key
属性还能用来决定哪些元素可以在下一个渲染中重用。特别是在动态列表渲染中其显得尤其重要。
React将新元素的key
值和之前的key
值进行比较:
- 新增的元素有一个新的
key
值 - 删除的元素的
key
值不再使用
很多React开发人员在使用.map()
来渲染列表时,习惯性将index
值赋值给key
。那么这样做是对还是错呢?如果是错误使用,我们又应该怎么来做才是较佳的方式。为了更好的理解,我们从简单的示例开始。
const Lists = ({initialList}) => {
const [lists, setLists] = React.useState(initialList)
const handleClick = (e) => {
setLists(lists.slice().reverse())
}
return (
<>
<ul className="list">
{lists.map((item, index) => <li className="list__item" key={index}>{item}<span>key={index}</span></li>)}
</ul>
<div className="action">
<button onClick={handleClick}><span>列表反转排列</span></button>
</div>
</>
)
}
const listsData = ['CSS', 'React', 'Vue', 'HTML5', 'JavaScript']
const rootElement = document.getElementById("app");
ReactDOM.render(<Lists initialList={listsData}/>, rootElement);
这个示例很简单,列表有五个列表项,其key
的值是数组listsData
的索引值index
,依次是0
至4
。当你点击按钮时,整个列表会反转排列。如下图所示:
虽然列表只是位置反转排列了(只需要交互一下DOM位置就行),但是React并不知道只改变了元素的位置,所以它会重新渲染整个列表(再次执行了虚拟DOM策略),这样一来就会大大增加DOM操作。大家都知道,React的高效是依赖于虚拟DOM策略。也就是说,能复用的话就应该尽量复用,没有必要的话绝对不碰DOM。
总而言之,对于列表元素来说,处理列表元素复用性会有一个问题:元素可能会在一个列表中改变位置。正如上例所示。
如果我们要修复这个问题,就需要给每个元素加上唯一的标识,这样React才能知道元素只是交换了位置。即,给每个列表项唯一的key
值,通过唯一的key
值为判断,列表只是更换位置(倒序排列),可以尽量复用元素内部的结构。因此,上面的示例,我们稍作调整:
<ul className="list">
{lists.map((item, index) => <li className="list__item" key={item.toString().toLocaleLowerCase()}>{item}<span>key={item.toString().toLocaleLowerCase()}</span></li>)}
</ul>
如果你坚持将key
的值设置为数组的index
索引值时,将会:
- 导致不必要的重新渲染(前面示例已演示过了)
- 当列表是不受控制的组件且仍然要使用
props
时会报错
为了提高列表渲染的性能,key
值强制为列表项的唯一值,一般可以通过后台给数据提供一个id
值。这样可以用于强制组件的完整的重新加载,对性能也非常有益。
如果你想对React中key
的使用进一步了解的话,可以阅读下面几篇文章:
小结
今天我们主要学习和探讨了在React中如何实现列表渲染。React中的列表渲染和条件渲染类似,都是React的基础部分,对于初学者而言,掌握这方面的知识是非常有必要的。文章开头简单地回顾了JavaScript中有关于循环迭代的一些基础知识,以及我们怎么使用JavaScript对数组、对象循环迭代,因为这部分是React中实现列表渲染最基础的部分。在React中我们拿到的数据一般都是数组或对象,所以要较好的实现列表渲染,就需要掌握这些JavaScript的知识,正如文章中的示例,我们使用的都是.map()
来实现列表渲染。
当然,在列表渲染中还有很多知识,文章中的都是最基础的部分,如果你在这方面有较深的经验,欢迎在下面的评论中与我们一起共享。