组件
组件定义
组件是 React
的核心概念,是 React
应用程序的基石。组件将应用的 UI
拆分成独立的、可复用的模块,React
应用程序正是由一个一个组件搭建而成的。
定义一个组件有两种方式,使用 ES6 class
(类组件)和使用函数(函数组件)。我们先介绍使用 class
定义组件的方式,使用函数定义组件的方式稍后介绍。
使用 class
定义组件需要满足两个条件:
-
class
继承自React.Component
。 -
class
内部必须定义render
方法,render
方法返回代表该组件UI
的React
元素。
使用 create-react-app
新建一个简易 BBS
项目,在这个项目中定义一个组件 PostList
,用于展示 BBS
的帖子列表。
PostList
的定义如下:
import React, { Component } from "react";
class PostList extends Component {
render() {
return (
<div>
帖子列表:
<ul>
<li>大家一起来讨论React吧</li>
<li>前端框架,你最爱哪一个</li>
<li>Web App的时代已经到来</li>
</ul>
</div>
);
}
}
export default PostList;
注意,在定义组件之后,使用 ES6 export
将 PostList
作为默认模块导出,从而可以在其他 JS
文件中导入 PostList
使用。现在页面上还无法显示出 PostList
组件,因为我们还没有将 PostList
挂载到页面的 DOM
节点上。需要使用 ReactDOM.render()
完成这一个工作:
import React from "react";
import ReactDOM from "react-dom";
import PostList from "./PostList";
ReactDOM.render(<PostList />, document.getElementById("root"));

注意,使用 ReactDOM.render()
需要先导入 react-dom
库,这个库会完成组件所代表的虚拟 DOM
节点到浏览器的 DOM
节点的转换。此时,页面展现在浏览器中,如图 2-1 所示。因为我们并没有为组件添加任何 CSS
样式,所以当前的页面效果还非常简陋,后续会逐步进行优化。本节项目源代码的目录为 /chapter-02/bbs-components
。
组件的props
在 2.2.1 小节中,PostList
中的每一个帖子都使用一个标签直接包裹,但一个帖子不仅包含帖子的标题,还会包含帖子的创建人、帖子创建时间等信息,这时候标签下的结构就会变得复杂,而且每一个帖子都需要重写一次这个复杂的结构,PostList
的结构将会变成类似这样的形式:
class PostList extends Component {
render() {
return (
<div>
帖子列表:
<ul>
<li>
<div>大家一起来讨论 React 吧</div>
<div>创建人:<span>张三</span></div>
<div>创建时间:<span>2017-09-01 10:00</span></div>
</li>
<li>
<div>前端框架,你最爱哪一个</div>
<div>创建人:<span>李四</span></div>
<div>创建时间:<span>2017-09-01 12:00</span></div>
</li>
<li>
<div>Web App的时代已经到来</div>
<div>创建人:<span>王五</span></div>
<div>创建时间:<span>2017-09-01 14:00</span></div>
</li>
</ul>
</div>
);
}
}
这样的结构显然很冗余,我们完全可以封装一个 PostItem
组件负责每一个帖子的展示,然后在 PostList
中直接使用 PostItem
组件,这样在 PostList
中就不需要为每一个帖子重复写一堆 JSX
标签。但是,帖子列表的数据依然存在于 PostList
中,如何将数据传递给每一个 PostItem
组件呢?这时候就需要用到组件的 props
属性。组件的 props
用于把父组件中的数据或方法传递给子组件,供子组件使用。在 2.1 节中,我们介绍了 JSX
标签的属性。props
是一个简单结构的对象,它包含的属性正是由组件作为 JSX
标签使用时的属性组成。例如下面是一个使用 User
组件作为 JSX
标签的声明:
<User name='React' age='4' address='America' >
此时 User
组件的 props
结构如下:
props = {
name: 'React',
age: '4',
address: 'America'
}
现在我们利用 props
定义 PostItem
组件:
import React, { Component } from "react";
class PostItem extends Component {
render() {
const { title, author, date } = this.props;
return (
<li>
<div>
{title}
</div>
<div>
创建人:<span>{author}</span>
</div>
<div>
创建时间:<span>{date}</span>
</div>
</li>
);
}
}
export default PostItem;
然后在 PostList
中使用 PostItem
:
import React, { Component } from "react";
import PostItem from "./PostItem";
const data = [
{ title: "大家一起来讨论React吧", author: "张三", date: "2017-09-01 10:00" },
{ title: "前端框架,你最爱哪一个", author: "李四", date: "2017-09-01 12:00" },
{ title: "Web App的时代已经到来", author: "王五", date: "2017-09-01 14:00" }
];
class PostList extends Component {
render() {
return (
<div>
帖子列表:
<ul>
{data.map(item =>
<PostItem
title={item.title}
author={item.author}
date={item.date}
/>
)}
</ul>
</div>
);
}
}
export default PostList;
此时,页面截图如图2-2所示。本节项目源代码的目录为 /chapter-02/bbs-components-props。

组件的state
组件的 state
是组件内部的状态,state
的变化最终将反映到组件 UI
的变化上。我们在组件的构造方法 constructor
中通过 this.state
定义组件的初始状态,并通过调用 this.setState
方法改变组件状态(也是改变组件状态的唯一方式),进而组件 UI
也会随之重新渲染。
下面来改造一下 BBS
项目。我们为每一个帖子增加一个 “点赞” 按钮,每点击一次,该帖子的点赞数增加 1。点赞数是会发生变化的,它的变化也会影响到组件 UI
,因此我们将点赞数 vote
作为 PostItem
的一个状态定义到它的 state
内。
import React, { Component } from "react";
class PostItem extends Component {
constructor(props) {
super(props);
this.state = {
vote: 0
};
}
// 处理点赞逻辑
handleClick() {
let vote = this.state.vote;
vote++;
this.setState({
vote: vote
});
}
render() {
const { title, author, date } = this.props;
return (
<li>
<div>
{title}
</div>
<div>
创建人:<span>{author}</span>
</div>
<div>
创建时间:<span>{date}</span>
</div>
<div>
<button
onClick={() => {
this.handleClick();
}}
>
点赞
</button>
<span>
{this.state.vote}
</span>
</div>
</li>
);
}
}
export default PostItem;
这里有三个需要注意的地方:
-
在组件的构造方法
constructor
内,首先要调用super(props)
,这一步实际上是调用了React.Component
这个class
的constructor
方法,用来完成React
组件的初始化工作。 -
在
constructor
中,通过this.state
定义了组件的状态。 -
在
render
方法中,我们为标签定义了处理点击事件的响应函数,在响应函数内部会调用this.setState
更新组件的点赞数。
新页面的截图如图 2-3 所示。本节项目源代码的目录为 /chapter-02/bbs-components-state。
通过 2.2.2 和 2.2.3 两个小节的介绍可以发现,组件的 props
和 state
都会直接影响组件的 UI
。事实上,React
组件可以看作一个函数,函数的输入是 props
和 state
,函数的输出是组件的 UI
。
UI = Component(props, state)

React
组件正是由 props
和 state
两种类型的数据驱动渲染出组件 UI
。props
是组件对外的接口,组件通过 props
接收外部传入的数据(包括方法);state
是组件对内的接口,组件内部状态的变化通过 state
来反映。另外,props
是只读的,你不能在组件内部修改 props
;state
是可变的,组件状态的变化通过修改 state
来实现。在第 4 章中,我们还会对 props
和 state
进行详细比较。
有状态组件和无状态组件
是不是每个组件内部都需要定义 state
呢?当然不是。state
用来反映组件内部状态的变化,如果一个组件的内部状态是不变的,当然就用不到 state
,这样的组件称之为 无状态组件,例如 PostList
。反之,一个组件的内部状态会发生变化,就需要使用 state
来保存变化,这样的组件称之为有状态组件,例如 PostItem
。
定义无状态组件除了使用 ES6 class
的方式外,还可以使用函数定义,也就是我们在本节开始时所说的函数组件。一个函数组件接收 props
作为参数,返回代表这个组件 UI
的 React
元素结构。例如,下面是一个简单的函数组件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
可以看出,函数组件的写法比类组件的写法要简洁很多,在使用无状态组件时,应该尽量将其定义成函数组件。
在开发 React
应用时,一定要先认真思考哪些组件应该设计成有状态组件,哪些组件应该设计成无状态组件。并且,应该尽可能多地使用无状态组件,无状态组件不用关心状态的变化,只聚焦于 UI
的展示,因而更容易被复用。React
应用组件设计的一般思路是,通过定义少数的有状态组件管理整个应用的状态变化,并且将状态通过 props
传递给其余的无状态组件,由无状态组件完成页面绝大部分 UI
的渲染工作。总之,有状态组件主要关注处理状态变化的业务逻辑,无状态组件主要关注组件 UI
的渲染。
下面让我们回过头来看一下 BBS
项目的组件设计。当前的组件设计并不合适,主要体现在:
-
帖子列表通过一个常量
data
保存在组件之外,但帖子列表的数据是会改变的,新帖子的增加或原有帖子的删除都会导致帖子列表数据的变化。 -
每一个
PostItem
都维持一个vote
状态,但除了vote
以外,帖子其他的信息(如标题、创建人等)都保存在PostList
中,这显然也是不合理的。
我们对这两个组件进行重新设计,将 PostList
设计为有状态组件,负责帖子列表数据的获取以及点赞行为的处理,将 PostItem
设计为无状态组件,只负责每一个帖子的展示。此时,PostList
和 PostItem
重构如下:
import React, { Component } from "react";
import PostItem from "./PostItem";
class PostList extends Component {
constructor(props) {
super(props);
this.state = {
posts: []
};
this.timer = null; // 定时器
this.handleVote = this.handleVote.bind(this); // ES 6 class中,必须手动绑定方法this的指向
}
componentDidMount() {
// 用setTimeout模拟异步从服务器端获取数据
this.timer = setTimeout(() => {
this.setState({
posts: [
{ id: 1, title: "大家一起来讨论React吧", author: "张三", date: "2017-09-01 10:00", vote: 0 },
{ id: 2, title: "前端框架,你最爱哪一个", author: "李四", date: "2017-09-01 12:00", vote: 0 },
{ id: 3, title: "Web App的时代已经到来", author: "王五", date: "2017-09-01 14:00", vote: 0 }
]
});
}, 1000);
}
componentWillUnmount() {
if(this.timer) {
clearTimeout(this.timer); // 清除定时器
}
}
handleVote(id) {
//根据帖子id进行过滤,找到待修改vote属性的帖子,返回新的posts对象
const posts = this.state.posts.map(item => {
const newItem = item.id === id ? {...item, vote: ++item.vote} : item;
return newItem;
})
// 使用新的posts对象设置state
this.setState({
posts
})
}
render() {
return (
<div>
帖子列表:
<ul>
{this.state.posts.map(item =>
<PostItem
post = {item}
onVote = {this.handleVote}
/>
)}
</ul>
</div>
);
}
}
export default PostList;
import React from "react";
function PostItem(props) {
const handleClick = () => {
props.onVote(props.post.id);
};
const { post } = props;
return (
<li>
<div>
{post.title}
</div>
<div>
创建人:<span>{post.author}</span>
</div>
<div>
创建时间:<span>{post.date}</span>
</div>
<div>
<button onClick={handleClick}>点赞</button>
<span>{post.vote}</span>
</div>
</li>
);
}
export default PostItem;
这里主要的修改有:
-
帖子列表数据定义为
PostList
组件的一个状态。 -
在
componentDidMount
生命周期方法中(关于组件的生命周期将在 2.3 节详细介绍)通过setTimeout
设置一个延时,模拟从服务器端获取数据,然后调用setState
更新组件状态。 -
将帖子的多个属性(ID、标题、创建人、创建时间、点赞数)合并成一个
post
对象,通过props
传递给PostItem
。 -
在
PostList
内定义handleVote
方法,处理点赞逻辑,并将该方法通过props
传递给PostItem
。 -
PostItem
定义为一个函数组件,根据PostList
传递的post
属性渲染UI
。当发生点赞行为时,调用props.onVote
方法将点赞逻辑交给PostList
中的handleVote
方法处理。
这样修改后,PostItem
只关注如何展示帖子,至于帖子的数据从何而来以及点赞逻辑如何处理,统统交给有状态组件 PostList
处理。组件之间解耦更加彻底,PostItem
组件更容易被复用。本节项目源代码的目录为 /chapter-02/bbs-components-stateless
。
属性校验和默认属性
我们已经知道,props
是一个组件对外暴露的接口,但到目前为止,组件内部并没有明显地声明它暴露出哪些接口,以及这些接口的类型是什么,这不利于组件的复用。幸运的是,React
提供了 PropTypes
这个对象,用于校验组件属性的类型。PropTypes
包含组件属性所有可能的类型,我们通过定义一个对象(对象的 key
是组件的属性名,value
是对应属性的类型)实现组件属性类型的校验。例如:
import PropTypes from 'prop-types';
function PostItem(props) {
//.....
}
PostItem.propTypes = {
post: PropTypes.object,
onVote: PropTypes.func
};
PropTypes
可以校验的组件属性类型见表2-1。
类型 | PropTypes对应属性 |
---|---|
String |
PropTypes.string |
Number |
PropTypes.number |
Boolean |
PropTypes.bool |
Function |
PropTypes.func |
Object |
PropTypes.object |
Array |
PropTypes.array |
Symbol |
PropTypes.symbol |
Element(React元素) |
PropTypes.element |
Node(可被渲染的节点:数字、字符串、React元素或由这些类型的数据组成的数组) |
PropTypes.node |
当使用 PropTypes.object
或 PropTypes.array
校验属性类型时,我们只知道这个属性是一个对象或一个数组,至于对象的结构或数组元素的类型是什么样的,依然无从得知。这种情况下,更好的做法是使用 PropTypes.shape 或 PropTypes.arrayOf。例如:
style: PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
}),
sequence: PropTypes.arrayOf(PropTypes.number)
表示 style
是一个对象,对象有 color
和 fontSize
两个属性,color
是字符串类型,fontSize
是数字类型;sequence
是一个数组,数组的元素是数字。
如果属性是组件的必需属性,也就是当使用某个组件时,必须传入的属性,就需要在 PropTypes
的类型属性上调用 isRequired
。在 BBS
项目中,对于 PostItem
组件,post
和 onVote
都是必需属性,PostItem
的 propTypes
定义如下:
PostItem.propTypes = {
post: PropTypes.shape({
id: PropTypes.number,
title: PropTypes.string,
author: PropTypes.string,
date: PropTypes.string,
vote: PropTypes.number
}).isRequired,
onVote: PropTypes.func.isRequired
}
本节项目源代码的目录为 /chapter-02/bbs-components-propTypes
。
React
还提供了为组件属性指定默认值的特性,这个特性通过组件的 defaultProps
实现。当组件属性未被赋值时,组件会使用 defaultProps
定义的默认属性。例如:
function Welcome(props) {
return <h1 className='foo'>Hello, {props.name}</h1>
}
Welcome.defaultProps = {
name: 'Stranger'
};
组件样式
到目前为止,我们还未对组件添加任何样式。本节将介绍如何为组件添加样式。为组件添加样式的方法主要有两种:外部 CSS
样式表和内联样式。
外部CSS样式表
这种方式和我们平时开发 Web
应用时使用外部 CSS
文件相同,CSS
样式表中根据 HTML
标签类型、ID
、class
等选择器定义元素的样式。唯一的区别是,React
元素要使用 className
来代替 class
作为选择器。例如,为 Welcome
组件的根节点设置一个 className='foo'
的属性:
function Welcome(props) {
return <h1 className="foo">Hello, {props.name}</h1>
}
然后在 CSS
样式表中通过 class
选择器定义 Welcome
组件的样式:
// style.css
.foo {
width: 100%;
height: 50px;
background-color: blue;
font-size: 20px;
}
样式表的引入方式有两种,一种是在使用组件的 HTML
页面中通过标签引入:
<link rel="stylesheet" type="text/css" href="style.css">
另一种是把样式表文件当作一个模块,在使用该样式表的组件中,像导入其他组件一样导入样式表文件:
import './style.css'; // 要保证相对路径设置正确
function Welcome(props) {
return <h1 className="foo">Hello, {props.name}</h1>
}
第一种引入样式表的方式常用于该样式表文件作用于整个应用的所有组件(一般是基础样式表);第二种引入样式表的方式常用于该样式表作用于某个组件(相当于组件的私有样式),全局的基础样式表也可以使用第二种方式引入,一般在应用的入口 JS
文件中引入。
使用 |
内联样式
内联样式实际上是一种 CSS in JS
的写法:将 CSS
样式写到 JS
文件中,用 JS
对象表示 CSS
样式,然后通过 DOM
类型节点的 style
属性引用相应样式对象。依然使用 Welcome
组件举例:
function Welcome(props) {
return (
<h1
style={{
width: "100%",
height: "50px",
backgroundColor: "blue",
fontSize: "20px"
}}
>
Hello, {props.name}
</h1>
);
}
style
使用了两个大括号,这可能会让你感到迷惑。其实,第一个大括号表示 style
的值是一个 JavaScript
表达式,第二个大括号表示这个 JavaScript
表达式是一个对象。换一种写法就容易理解了:
function Welcome(props) {
const style = {
width: "100%",
height: "50px",
backgroundColor: "blue",
fontSize: "20px"
};
return <h1 style={style}>Hello, {props.name}</h1>
}
当使用内联样式时,还有一点需要格外注意:样式的属性名必须使用驼峰格式的命名。所以,在 Welcome
组件中,background-color
写成 backgroundColor
,font-size
写成 fontSize
。
下面为 BBS
项目增加一些样式。创建 style.css
、PostList.css
和 PostItem.css
三个样式文件,三个样式表分别在 index.html
、PostList.js
、PostItem.js
中引入。样式文件如下:
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
ul {
list-style: none;
}
h2 {
text-align: center;
}
.container {
width: 900px;
margin: 20px auto;
}
.item {
border-top: 1px solid grey;
padding: 15px;
font-size: 14px;
color: grey;
line-height: 21px;
}
.title {
font-size: 16px;
font-weight: bold;
line-height: 24px;
color: #009a61;
}
.like {
width: 100%;
height: 20px;
}
.like img{
width: 20px;
height: 20px;
}
.like span{
width: 20px;
height: 20px;
vertical-align: middle;
display: table-cell;
}
这里需要提醒一下,style.css
放置在 public
文件夹下,PostList.css
和 PostItem.css
放置在 src
文件夹下。create-react-app
将 public
下的文件配置成可以在 HTML
页面中直接引用,因此我们将 style.css
放置在 public
文件夹下。而 PostList.css
和 PostItem.css
是以模块的方式在 JS
文件中被导入的,因此放置在 src
文件夹下。
我们还将 PostItem
中的点赞按钮换成了图标,图标也可以作为一个模块被 JS
文件导入,如 PostItem.js
所示:
import React from "react";
import "./PostItem.css";
import like from "./images/like-default.png"; // 图标作为模块被导入
function PostItem(props) {
const handleClick = () => {
props.onVote(props.post.id);
};
const { post } = props;
return (
<li className='item'>
<div className='title'>
{post.title}
</div>
<div>
创建人:<span>{post.author}</span>
</div>
<div>
创建时间:<span>{post.date}</span>
</div>
<div className='like'>
<span><img alt='vote' src={like} onClick={handleClick} /></span>
<span>{post.vote}</span>
</div>
</li>
);
}
export default PostItem;
增加样式后的页面截图如图 2-4 所示。本节项目源代码的目录为 /chapter-02/bbs-components-style
。

组件和元素
在2.1节介绍过 React
元素的概念。React
组件和元素这两个概念非常容易混淆。React
元素是一个普通的 JavaScript
对象,这个对象通过 DOM
节点或 React
组件描述界面是什么样子的。JSX
语法就是用来创建 React
元素的(不要忘了,JSX
语法实际上是调用了 React.createElement
方法)。例如:
// Button是一个自定义的React组件
<div className='foo'>
<Button color='blue'>OK</Button>
</div>
上面的 JSX
代码会创建下面的 React
元素:

React
组件是一个 class
或函数,它接收一些属性作为输入,返回一个 React
元素。React
组件是由若干 React
元素组建而成的。通过下面的例子,可以解释 React
组件与 React
元素间的关系。
