第2章 React的组件
在React中,组件是应用程序的基石,页面中所有的界面和功能都是由组件堆积而成的。在前端组件化开发之前,一个页面可能会有成百上千行代码逻辑写在一个.js文件中,这种代码可读性很差,如果增加功能,很容易出现一些意想不到的问题。合理的组件设计有利于降低系统各个功能的耦合性,并提高功能内部的聚合性。这对于前端工程化及降低代码维护成本来说,是非常必要的。
本章主要介绍React中组件的创建、成员、通信和生命周期,最后会通过一个实战案例——TodoList演示组件的使用。
2.1 组件的声明方式
简单来说,在React中创建组件的方式有3种。
- ES 5写法:React.createClass()(老版本用法,不建议使用);
- ES 6写法:React.Component;
- 无状态的函数式写法,又称为纯组件SFC。
2.1.1 ES 5写法:React.createClass()
React.createClass()是React刚出现时官方推荐的创建组件方式,它使用ES 5原生的JavaScript来实现React组件。React.createClass()这个方法构建一个组件“类”,它接受一个对象为参数,对象中必须声明一个render()方法,render()方法将返回一个组件实例。
使用React.createClass()创建组件示例:
var Input = React.createClass({
// 定义传入props中的各种属性类型
propTypes: {
initialValue: React.PropTypes.string
},
//组件默认的props对象
defaultProps: {
initialValue: ''
},
// 设置initial state
getInitialState: function() {
return {
text: this.props.initialValue || 'placeholder'
};
},
handleChange: function(event) {
this.setState({
text: event.target.value
});
},
render: function() {
return (
<div>
Type something:
<input onChange={this.handleChange} value={this.state.text} />
</div>
);
}
});
createClass()本质上是一个工厂函数。createClass()声明的组件方法的定义使用半角逗号隔开,因为creatClass()本质上是一个函数,传递给它的是一个Object。通过propTypes对象和getDefaultProps()方法来设置props类型和获取props。createClass()内的方法会正确绑定this到React类的实例上,这也会导致一定的性能开销。React早期版本使用该方法,而在新版本中该方法被废弃,因此不建议读者使用。
2.1.2 ES 6写法:React.Component
React.Component是以ES 6的形式来创建组件的,这是React目前极为推荐的创建有状态组件的方式。相对于React.createClass(),此种方式可以更好地实现代码复用。本节将2.1.1节介绍的React.createClass()形式改为React.Component形式。
使用React.Component创建组件示例:
class Input extends React.Component {
constructor(props) {
super(props);
// 设置initial state
this.state = {
text: props.initialValue || 'placeholder'
};
// ES 6类中的函数必须手动绑定
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({
text: event.target.value
});
}
render() {
return (
<div>
Type something:
<input onChange={this.handleChange}
value={this.state.text} />
</div>
);
}
}
React.Component创建的组件,函数成员不会自动绑定this,需要开发者手动绑定,否则this无法获取当前组件的实例对象。当然绑定this的方法有多种,除了上面的示例代码中在constructor()中绑定this外,最常见的还有通过箭头函数来绑定this,以及在方法中直接使用bind(this)来绑定这两种。
通过箭头函数来绑定this示例:
// 使用bind来绑定
<div onClick={this.handleClick.bind(this)}></div>
在方法中直接使用bind(this)来绑定this示例:
// 使用arrow function来绑定
<div onClick={()=>this.handleClick()}></div>
2.1.3 无状态组件
下面来看看无状态组件,它是React 0.14之后推出的。如果一个组件不需要管理state,只是单纯地展示,那么就可以定义成无状态组件。这种方式声明的组件可读性好,能大大减少代码量。无状态函数式组件可以搭配箭头函数来写,更简洁,它没有React的生命周期和内部state。
无状态函数式组件示例:
const HelloComponent = (props) =>(
?<div>Hello {props.name}</div>?
)
ReactDOM.render(<HelloComponent?name="marlon"?/>, mountNode)
无状态函数式组件在需要生命周期时,可以搭配高阶组件(HOC)来实现。无状态组件作为高阶组件的参数,高阶组件内存放需要的生命周期和状态,其他只负责展示的组件都使用无状态函数式的组件来写。
有生命周期的函数式组件示例:
import React from 'react';
export const Table = (ComposedComponent) => {
return class extends React.Component {
constructor(props) {
super(props)
}
componentDidMount() {
console.log('componentDidMount');
}
render() {
return (
<ComposedComponent {...this.props}/>
)
}
}
}
注意:React 16.7.0-alpha(内测)中引入了Hooks,这使得在函数式组件内可以使用state和其他React特性。
2.2 组件的主要成员
在React中,数据流是单向流动的,从父节点向子节点传递(自上而下)。子组件可以通过属性props接收来自父组件的状态,然后在render()方法中渲染到页面。每个组件同时又拥有属于自己内部的状态state,当父组件中的某个属性发生变化时,React会将此改变了的状态向下递归遍历组件树,然后触发相应的子组件重新渲染(re-render)。
如果把组件视为一个函数,那么props就是从外部传入的参数,而state可以视为函数内部的参数,最后函数返回虚拟DOM。
本节将学习组件中最重要的成员state和props。
2.2.1 状态(state)
每个React组件都有自己的状态,相比于props,state只存在于组件自身内部,用来影响视图的展示。可以使用React内置的setState()方法修改state,每当使用setState()时,React会将需要更新的state合并后放入状态队列,触发调和过程(Reconciliation),而不是立即更新state,然后根据新的状态结构重新渲染UI界面,最后React会根据差异对界面进行最小化重新渲染。
React通过this.state访问状态,调用this.setState()方法来修改状态。
React访问状态示例:
(源码地址为https://jsfiddle.net/allan91/etbj6gsx/1/)
class App extends React.Component {
constructor(props){
super(props);
this.state = {
data: 'World'
}
}
render(){
return(
<div>
Hello, {this.state.data}
</div>
)
}
}
ReactDOM.render(
<App />,
document.querySelector(''#app'') // App组件挂载到ID为app的DOM元素上
)
上述代码中,App组件在UI界面中展示了自身的状态state。下面使用setState()修改这个状态。
React修改状态示例:
(源码地址为https://jsfiddle.net/allan91/etbj6gsx/3/)
class App extends React.Component {
constructor(props){
super(props);
this.state = {
data: 'World'
}
}
handleClick = () => {
this.setState({
data:'Redux'
})
}
render(){
return(
<div>
Hello, {this.state.data}
<button onClick={this.handleClick}>更新</button>
</div>
)
}
}
ReactDOM.render(
<App />,
document.querySelector("#app")
)
上述代码中通过单击“更新”按钮使用setState()方法修改了state值,触发UI界面更新。本例状态更改前后的展示效果,如图2.1所示。
2.2.2 属性(props)
state是组件内部的状态,那么组件之间如何“通信”呢?这就是props的职责所在了。通俗来说,props就是连接各个组件信息互通的“桥梁”。React本身是单向数据流,所以在props中数据的流向非常直观,并且props是不可改变的。props的值只能从默认属性和父组件中传递过来,如果尝试修改props,React将会报出类型错误的提示。
props示例应用:
(源码地址为https://jsfiddle.net/n5u2wwjg/35076/)
function Welcome(props) {
return <p>Hello, {props.name}</p>
}
function App(){
return (
<Welcome name='world' /> // 引用Welcome组件,name为该组件的属性
)
}
ReactDOM.render(
<App />,
document.querySelector("#app")
)
上述代码使用了函数定义组件。被渲染的App组件内引用了一个外部组件Welcome,并在该组件内定义了一个名为name的属性,赋值为world。Welcome组件接收到来自父组件的name传递,在界面中展示Hello,World。
当然,也可以使用class来定义一个组件:
class Welcome extends React.Component{
render() {
return <p>Hello, {this.props.name}</p>;
}
}
这个Welcome组件与上面函数式声明的组件在React中的效果是一样的。
2.2.3 render()方法
render()方法用于渲染虚拟DOM,返回ReactElement类型。
元素是React应用的最小单位,用于描述界面展示的内容。很多初学者会将元素与“组件”混淆。其实,元素只是组件的构成,一个元素可以构成一个组件,多个元素也可以构成一个组件。render()方法是一个类组件必须拥有的特性,其返回一个JSX元素,并且外层一定要使用一个单独的元素将所有内容包裹起来。比如:
render() {
return(
<div>a</div>
<div>b</div>
<div>c</div>
)
}
上面这样是错误的,外层必须有一个单独的元素去包裹:
render() {
return(
<div>
<div>a</div>
<div>b</div>
<div>c</div>
</div>
)
}
1.render()返回元素数组
2017年9月,React发布的React 16版本中为render()方法新增了一个“支持返回数组组件”的特性。在React 16版本之后无须将列表项包含在一个额外的元素中了,可以在 render()方法中返回元素数组。需要注意的是,返回的数组跟其他数组一样,需要给数组元素添加一个key来避免key warning。
render()方法返回元素数组示例:
(源码地址为https://jsfiddle.net/n5u2wwjg/35080/)
render() {
return [
<div key="a">a</div>,
<div key="b">b</div>,
<div key="c">c</div>,
];
}
除了使用数组包裹多个同级子元素外,还有另外一种写法如下:
import React from 'react';
export default function () {
return (
<>
<div>a</div>
<div>b</div>
<div>c</div>
</>
);
}
简写的<>>其实是React 16中React.Fragment的简写形式,不过它对于部分前端工具的支持还不太好,建议使用完整写法,具体如下:
import React from 'react';
export default function () {
return (
<React.Fragment>
<div>a</div>
<div>b</div>
<div>c</div>
</React.Fragment>
);
}
最后输出到页面的标签也能达到不可见的效果,也就是在同级元素外层实际上是没有包裹其他元素的,这样能减少DOM元素的嵌套。
2.render()返回字符串
当然,render()方法也可以返回字符串。
render()方法返回字符串示例:
(源码地址为https://jsfiddle.net/n5u2wwjg/35079/)
render() {
return 'Hello World';
}
运行程序,界面中将展示以上这段字符串。
3.render()方法中的变量与运算符&&
render()方法中可以使用变量有条件地渲染要展示的页面。常见做法是通过花括号{}包裹代码,在JSX中嵌入任何表达式,比如逻辑与&&。
render()方法中使用运算符示例:
const fruits = ['apple', 'orange', 'banana'];
function Basket(props) {
const fruitsList = props.fruits;
return (
<div>
<p>I have: </p>
{fruitsList.length > 0 &&
<span>{fruitsList.join(', ')}</span>
}
</div>
)
}
ReactDOM.render(<Basket fruits={fruits}/>, document.querySelector("#app"))
上述代码表示,如果从外部传入Basket组件的数组不为空,也就是表达式左侧为真, &&右侧的元素就会被渲染。展示效果如图2.2所示。如果表达式左侧为false,&&右侧元素就会被React忽略渲染。
4.render()方法中的三目运算符
在render()方法中还能使用三目运算符condition ? true : false。
在render()方法中使用三目运算符示例:
(源码地址为https://jsfiddle.net/n5u2wwjg/35239/)
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
isUserLogin: false
}
}
render() {
const { isUserLogin } = this.state;
return (
<div>
{ isUserLogin ? <p>已登录</p> : <p>未登录</p> }
</div>
)
}
}
ReactDOM.render(<App/>, document.querySelector("#app"))
上述代码根据isUserLogin的真和假来动态显示p标签的内容,当然也可以动态展示封装好的组件,例如:
return (
<div>
{ isUserLogin ? <ComponentA /> : <ComponentB /> }
</div>
)
2.3 组件之间的通信
React编写的应用是以组件的形式堆积而成的,组件之间虽相互独立,但相互之间还是可以通信的。本节将介绍组件中的几种通信方式。
2.3.1 父组件向子组件通信
前面章节已经提到过,React的数据是单向流动的,只能从父级向子级流动,父级通过props属性向子级传递信息。
父组件向子组件通信示例:
(源码地址为https://jsfiddle.net/n5u2wwjg/35403/)
class Child extends React.Component {
render (){
return (
<div>
<h1>{ this.props.fatherToChild }</h1>
</div>
)
}
}
class App extends React.Component {
render() {
let data = 'This message is from Dad!'
return (
<Child fatherToChild={ data } />
)
}
}
ReactDOM.render(
<App/>,
document.querySelector("#app")
)
上述代码中有两个组件:子组件Child和父组件App。子组件在父组件中被引用,然后在父组件内给子组件定了一个props:fatherToChild,并将父组件的data传递给子组件中展示。
注意:父组件可以通过props向子组件传递任何类型。
2.3.2 子组件向父组件通信
虽然React数据流是单向的,但并不影响子组件向父组件通信。通过父组件可以向子组件传递函数这一特性,利用回调函数来实现子组件向父组件通信。当然也可以通过自定义事件机制来实现,但这种场景会显得过于复杂。所以为了简单方便,还是利用回调函数来实现。
子组件向父组件通信示例:
class Child extends React.Component {
render (){
return <input type="text" onChange={(e)=>this.props.handleChange
(e.target.value)} />
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: ''
}
}
handleChange = text => {
this.setState({
data: text
})
}
render() {
return (
<div>
<p>This message is from Child:{this.state.data}</p>
<Child handleChange={ this.handleChange } />
</div>
)
}
}
ReactDOM.render(
<App/>,
document.querySelector("#app")
)
上述代码中有两个组件:子组件Child和父组件App。子组件被父组件引用,在父组件中定义了一个handleChange事件,并通过props传给子组件让子组件调用该方法。子组件接收到来自父组件的handleChange方法,当子组件input框内输入的值Value发生变化时,就会触发handleChange方法,将该值传递给父组件,从而达到子对父通信。
注意:一般情况下,回调函数会与setState()成对出现。
2.3.3 跨级组件通信
当组件层层嵌套时,要实现跨组件通信,首先会想到利用props一层层去传递信息。虽然可以实现信息传递,但这种写法会显得有点“啰嗦”,也不优雅。这种场景在React中,一般使用context来实现跨级父子组件通信。
context的设计目的就是为了共享对于一个组件树而言是“全局性”的数据,可以尽量减少逐层传递,但并不建议使用context。因为当结构复杂的时候,这种全局变量不易追溯到源头,不知道它是从哪里传递过来的,会导致应用变得混乱,不易维护。
context适用的场景最好是全局性的信息,且不可变的,比如用户信息、界面颜色和主题制定等。
context实现的跨级组件通信示例(React 16.2.0):
(源码地址为https://jsfiddle.net/allan91/Lbecjy18/2/)
// 子(孙)组件
class Button extends React.Component {
render() {
return (
<button style={{background: this.context.color}}>
{this.props.children}
</button>
);
}
}
// 声明contextTypes用于访问MessageList中定义的context数据
Button.contextTypes = {
color: PropTypes.string
};
// 中间组件
class Message extends React.Component {
render() {
return (
<div>
<Button>Delete</Button>
</div>
);
}
}
// 父组件
class MessageList extends React.Component {
// 定义context需要实现的方法
getChildContext() {
return {
color: "orange"
};
}
render() {
return <Message />;
}
}
// 声明context类型
MessageList.childContextTypes = {
color: PropTypes.string
};
ReactDOM.render(
<MessageList />,
document.getElementById('container')
);
上述代码中,MessageList为context的提供者,通过在MessageList中添加childContextTypes和getChildContext()和MessageList。React会向下自动传递参数,任何组织只要在它的子组件中(这个例子中是Button),就能通过定义contextTypes来获取参数。如果contextTypes没有定义,那么context将会是个空对象。
context中有两个需要理解的概念:一个是context的生产者(provider);另一个是context的消费者(consumer),通常消费者是一个或多个子节点。所以context的设计模式是属于生产-消费者模式。在上述示例代码中,生产者是父组件MessageList,消费者是孙组件Button。
在React中,context被归为高级部分(Advanced),属于React的高级API,因此官方不推荐在不稳定的版本中使用。值得注意的是,很多优秀的React第三方库都是基于context来完成它们的功能的,比如路由组件react-route通过context来管理路由,react-redux的通过context提供全局Store,拖曳组件react-dnd通过context分发DOM的Drag和Drop事件等。
注意:不要仅仅为了避免在几个层级下的组件传递props而使用context,context可用于多个层级的多个组件需要访问相同数据的情景中。
2.3.4 非嵌套组件通信
非嵌套组件就是没有包含关系的组件。这类组件的通信可以考虑通过事件的发布-订阅模式或者采用context来实现。
如果采用context,就是利用组件的共同父组件的context对象进行通信。利用父级实现中转传递在这里不是一个好的方案,会增加子组件和父组件之间的耦合度,如果组件层次嵌套较深的话,不易找到父组件。
那么发布-订阅模式是什么呢?发布-订阅模式又叫观察者模式。其实很简单,举个现实生活中的例子:
很多人手机上都有微信公众号,读者所关注的公众号会不定期推送信息。
这就是一个典型的发布-订阅模式。在这里,公众号就是发布者,而关注了公众号的微信用户就是订阅者。关注公众号后,一旦有新文章或广告发布,就会推送给订阅者。这是一种一对多的关系,多个观察者(关注公众号的微信用户)同时关注、监听一个主体对象(某个公众号),当主体对象发生变化时,所有依赖于它的对象都将被通知。
发布-订阅模式有以下优点:
- 耦合度低:发布者与订阅者互不干扰,它们能够相互独立地运行。这样就不用担心开发过程中这两部分的直接关系。
- 易扩展:发布-订阅模式可以让系统在无论什么时候都可进行扩展。
- 易测试:能轻易地找出发布者或订阅者是否会得到错误的信息。
- 灵活性:只要共同遵守一份协议,不需要担心不同的组件是如何组合在一起的。
React在非嵌套组件中只需要某一个组件负责发布,其他组件负责监听,就能进行数据通信了。下面通过代码来演示这种实现。
非嵌套组件通信示例:
(1)安装一个现成的events包:
npm install events —save
(2)新建一个公共文件events.js,引入events包,并向外提供一个事件对象,供通信时各个组件使用:
import { EventEmitter } from "events";
export default new EventEmitter();
(3)组件App.js:
import React, { Component } from 'react';
import ComponentA from "./ComponentA";
import ComponentB from "./ComponentA";
import "./App.css";
export default class App extends Component{
render(){
return(
<div>
<ComponentA />
<ComponentB />
</div>
);
}
}
(4)组件ComponentA:
import React,{ Component } from "react";
import emitter from "./events";
export default class ComponentA extends Component{
constructor(props) {
super(props);
this.state = {
data: React,
};
}
componentDidMount(){
// 组件加载完成以后声明一个自定义事件
// 绑定callMe事件,处理函数为addListener()的第2个参数
this.eventEmitter = emitter.addListener("callMe",(data)=>{
this.setState({
data
})
});
}
componentWillUnmount(){
// 组件销毁前移除事件监听
emitter.removeListener(this.eventEmitter);
}
render(){
return(
<div>
Hello,{ this.state.data }
</div>
);
}
}
(5)组件ComponentB:
import React,{ Component } from "react";
import emitter from "./events";
export default class ComponentB extends Component{
render(){
const cb = (data) => {
return () => {
// 触发自定义事件
// 可传多个参数
emitter.emit("callMe", "World")
}
}
return(
<div>
<button onClick = { cb("Hey") }>点击</button>
</div>
);
}
}
当在非嵌套组件B内单击按钮后,会触发emitter.emit(),并且将字符串参数World传给callMe。组件A展示的内容由Hello,React变为Hello,World。这就是一个典型的非嵌套组件的通信。
注意:组件之间的通信要保持简单、干净,如果遇到了非嵌套组件通信,这时候读者需要仔细审查代码设计是否合理。要尽量避免使用跨组件通信和非嵌套组件通信等这类情况。
2.4 组件的生命周期
生命周期(Life Cycle)的概念应用很广泛,特别是在政治、经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”(Cradle-to- Grave)的整个过程。在React组件的整个生命周期中,props和state的变化伴随着对应的DOM展示。每个组件提供了生命周期钩子函数去响应组件在不同时刻应该做和可以做的事情:创建时、存在时、销毁时。
本节将从React组件的“诞生”到“消亡”来介绍React的生命周期。由于React 16版本中对生命周期有所修改,所以本节只介绍最新版本的内容,React 15版本的生命周期不推荐使用,如需了解请读者自行查阅。这里以React 16.4以上版本为例讲解。
2.4.1 组件的挂载
React将组件渲染→构造DOM元素→展示到页面的过程称为组件的挂载。一个组件的挂载会经历下面几个过程:
- constructor();
- static getDerivedStateFromProps();
- render();
- componentDidMount()。
组件的挂载示例:
(源码地址为https://jsfiddle.net/allan91/n5u2wwjg/225709/)
class App extends React.Component {
constructor(props) {
super(props);
console.log("constructor")
}
static getDerivedStateFromProps(){
console.log("getDerivedStateFromProps")
return null;
}
// React 17中将会移除componentWillMount()
// componentWillMount() {
// console.log("componentWillMount")
//}
render() {
console.log("render")
return 'Test'
}
// render()之后构造DOM元素插入页面
componentDidMount() {
console.log("componentDidMount")
}
}
ReactDOM.render(
<App/>,
document.querySelector("#app")
)
打开控制台,上述代码执行后将依次打印:
constructor
getDerivedStateFromProps
render
componentDidMount
constructor()是ES 6中类的默认方法,通过new命令生成对象实例时自动调用该方法。其中的super()是class方法中的继承,它是使用extends关键字来实现的。子类必须在constructor()中调用super()方法,否则新建实例会报错。如果没有用到constructor(),React会默认添加一个空的constructor()。
getDerivedStateFromProps()在组件装载时,以及每当props更改时被触发,用于在props(属性)更改时更新组件的状态,返回的对象将会与当前的状态合并。
componentDidMount()在组件挂载完成以后,也就是DOM元素已经插入页面后调用。而且这个生命周期在组件挂载过程中只会执行一次,通常会将页面初始数据的请求在此生命周期内执行。
注意:其中被注释的componentWillMount()是React旧版本中的生命周期,官方不建议使用这个方法,以后会被移除,因此这里不做介绍。
2.4.2 数据的更新过程
组件在挂载到DOM树之后,当界面进行交互动作时,组件props或state改变就会触发组件的更新。假如父组件render()被调用,无论此时props是否有改变,在render()中被渲染的子组件就会经历更新过程。一个组件的数据更新会经历下面几个过程:
- static getDerivedStateFromProps();
- shouldComponentUpdate();
- componentWillUpdate()/UNSAFE_componentWillUpdate();
- render();
- getSnapshotBeforeUpdate();
- componentDidUpdate()。
数据更新可以分为下面两种情况讨论:
1.组件自身state更新
组件自身state更新会依次执行:
shouldComponentUpdate()—> render()—> getSnapBeforeUpdate()—> componentDidUpdate()
2.父组件props更新
父组件props更新会依次执行:
static getDerivedStateFromProps() —> shouldComponentUpdate()—> render()—> getSnapBeforeUpdate()—> componentDidUpdate()
相对于自身state更新,这里多了一个getDerivedStateFromProps()方法,它的位置是组件在接收父组件props传入后和渲染前setState()的时期,当挂载的组件接收到新的props时被调用。此方法会比较this.props和nextProps并使用this.setState()执行状态转换。
上面两种更新的顺序情况基本相同,下面来看看它们分别有何作用和区别:
- shouldComponentUpdate(nextProps, nextState):用于判断组件是否需要更新。它会接收更新的props和state,开发者可以在这里增加判断条件。手动执行是否需要去更新,也是React性能优化的一种手法。默认情况下,该方法返回true。当返回值为false时,则不再向下执行其他生命周期方法。
- componentDidUpdate(object nextProps, object nextState):很容易理解,从字面意思就知道它们分别代表组件render()渲染后的那个时刻。componentDidUpdate()方法提供了渲染后的props和state。
注意:无状态函数式组件没有生命周期,除了React 16.7.0的新特性Hooks。
2.4.3 组件的卸载(unmounting)
React提供了一个方法:componentWillUnmount()。当组件将要被卸载之前调用,可以在该方法内执行任何可能需要清理的工作。比如清除计时器、事件回收、取消网络请求,或清理在componentDidMount()中创建的任何监听事件等。
组件的卸载示例:
import React, { Component } from "react";
export default class Hello extends Component {
componentDidMount() {
this.timer = setTimeout(() => {
console.log("挂在this上的定时器");
}, 500);
}
componentWillUnmount() {
this.timer && clearTimeout(this.timer);
}
}
2.4.4 错误处理
在渲染期间,生命周期方法或构造函数constructor()中发生错误时将会调用componentDidCatch()方法。
React错误处理示例:
import React from "react";
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, info) {
this.setState({
hasError: true
});
}
render() {
if (this.state.hasError) {
return <h1>这里可以自定义一些展示,这里的内容能正常渲染。</h1>;
}
return this.props.children;
}
}
在componentDidCatch()内部把hasError状态设置为true,然后在渲染方法中检查这个状态,如果出错状态是true,就渲染备用界面;如果状态是false,就正常渲染应该渲染的界面。
错误边界不会捕获下面的错误:
- 错误边界本身错误,而非子组件抛出的错误。
- 服务端渲染(Server side rendering)。
- 事件处理(Event handlers),因为事件处理不发生在React渲染时,报错不影响渲染)。
- 异步代码。
2.4.5 老版React中的生命周期
老版本的React中还有如下生命周期:
- componentWillMount();
- componentWillReceiveProps();
- componentWillUpdate()。
老版本中的部分生命周期方法有多种方式可以完成一个任务,但很难弄清楚哪个才是最佳选项。有的错误处理行为会导致内存泄漏,还可能影响未来的异步渲染模式等。鉴于此,React决定在未来废弃这些方法。
React官方考虑到这些改动会影响之前一直在使用生命周期方法的组件,因此将尽量平缓过渡这些改动。在React 16.3版本中,为不安全生命周期引入别名:
- UNSAFE_componentWillMount;
- UNSAFE_componentWillReceiveProps;
- UNSAFE_componentWillUpdate。
旧的生命周期名称和新的别名都可以在React16.3版本中使用。将要废弃旧版本的生命周期会保留至React 17版本中删除。
同时,React官方也提供了两个新的生命周期:
- getDerivedStateFromProps();
- getSnapshotBeforeUpdate()。
getDerivedStateFromProps()生命周期在组件实例化及接收新props后调用,会返回一个对象去更新state,或返回null不去更新,用于确认当前组件是否需要重新渲染。这个生命周期将可以作为componentWillReceiveProps()的安全替代者。
getDerivedStateFromProps()生命周期示例:
class App extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
...
}
}
getSnapshotBeforeUpdate()生命周期方法将在更新之前被调用,比如DOM被更新之前。这个生命周期的返回值将作为第3个参数传递给componentDidUpdate()方法,虽然这个方法不经常使用,但是对于一些场景(比如保存滚动位置)非常有用。配合componentDidUpdate()方法使用,新的生命周期将覆盖旧版componentWillUpdate()的所有用例。
getSnapshotBeforeUpdate()生命周期(官方示例):
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 是否添加新项目到列表
// 捕获滚动定位用于之后调整滚动位置
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current;
return list.scrollHeight - list.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 如果有新值,就添加进新项目
// 调整滚动位置,新项目不会把老项目推到可视窗口外
// (这里的snapshot来自于getSnapshotBeforeUpdate()这个生命周期的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
<div ref={this.listRef}>{/- ...contents... */}</div>
);
}
}
2.4.6 生命周期整体流程总结
React组件的整个生命周期流程图如图2.3所示来描述。
2.5 组件化实战训练——TodoList
前面章节中学习了如何配置Webpack来搭建Hello World项目,以及React的组件、组件通信和生命周期等。接下来继续基于前面的这个项目来实现一个简单的TodoList,以此加深读者对组件化的了解。
在这个简单的TodoList项目中,需要实现:
- 通过input输入框输入todo内容;
- 单击Submit按钮将输入的内容展示在页面上。
在1.5节脚手架中,Webpack的loader只对JS和JSX做了识别,现在需要在项目中加入CSS的相关loader,目的是让Webpack识别和加载样式文件。
(1)安装CSS的相关loader:
npm install css-loader style-loader --save-dev
(2)配置Webpack中的loader:
var webpack = require("webpack");
var path = require("path");
const CleanWebpackPlugin = require("clean-webpack-plugin");
var BUILD_DIR = path.resolve(__dirname, "dist");
var APP_DIR = path.resolve(__dirname, "src");
const HtmlWebpackPlugin = require("html-webpack-plugin");
var config = {
entry: APP_DIR + "/index.jsx",
output: {
path: BUILD_DIR,
filename: "bundle.js"
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
test: /\.css$/, // 只加载.css文件
loader: 'style-loader!css-loader' // 同时运行两个loader
}
]
},
devServer: {
port: 3000,
contentBase: "./dist"
},
plugins: [
new HtmlWebpackPlugin({
template: "index.html",
// favicon: 'theme/img/favicon.ico',
inject: true,
sourceMap: true,
chunksSortMode: "dependency"
}),
new CleanWebpackPlugin(["dist"])
]
};
module.exports = config;
至此,TodoList的项目脚手架配置结束。
(3)接下来是相应组件的代码,入口页面App.jsx负责渲染组件头部Header和列表ListItems,并在当前组件内部state维护列表的项目和输入的内容。
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
todoItem: "",
items: []
};
}
render() {
return (
<div>
</div>
);
}
}
从上述代码可以看到App组件的state内有todoItem和items。todoItem用于存储输入框输入的值;items用于存储输入框内提交的值,之后用于列表的渲染。
(4)再来编写输入框输入内容时的onChange事件:
onChange(event) {
this.setState({
todoItem: event.target.value
});
}
<input value={this.state.todoItem} onChange={this.onChange} />
从上述代码中可以看到,input的值来自于App组件内的state。用户每次输入后,onChange事件监听其变化,然后调用this.setState()将改变的值实时写入input中展示。
(5)表单提交:
onSubmit(event) {
event.preventDefault();
this.setState({
todoItem: "",
items: [
...this.state.items,
this.state.todoItem
]
});
}
<form className="form-wrap" onSubmit={this.onSubmit}>
<input value={this.state.todoItem} onChange={this.onChange} />
<button>Submit</button>
</form>
当单击Submit按钮时,输入框的值将通过表单提交的方式触发onSubmit事件,然后调用this.setState()添加输入框中的值到items数组,同时清空输入框。
(6)将内容整理为3部分:头Header、表单form和列表ListItems。其中,Header和ListItems各为一个组件。
./src/Header.js内容如下:
import React from 'react';
const Header = props => (
<h1>{props.title}</h1>
);
export default Header;
./src/ListItems.js内容如下:
import React from 'react';
const ListItems = props => (
<ul>
{
props.items.map(
(item, index) => <li key={index}>{item}</li>
)
}
</ul>
);
export default ListItems;
Header和ListItems都是无状态函数式组件,接收父级./src/app.jsx传入的props数据,用于各自的展示。
(7)在入口./src/app.jsx 中引入组件:
import React, { Component } from "react";
import { render } from "react-dom";
+ import ListItems from "./ListItems";
+ import Header from "./Header";
(8)引入样式:
import React, { Component } from "react";
import { render } from "react-dom";
import ListItems from "./ListItems";
import Header from "./Header";
+ import "./index.css";
至此,所有内容完成,此时这个项目的结构如下:
.
├── README.md
├── index.html
├── package-lock.json
├── package.json
├── src
│ ├── Header.js
│ ├── ListItems.js
│ ├── app.jsx
│ └── index.css
└── webpack.config.js
最终入口app.jsx文件的代码如下:
/src/app.jsx内容如下:
import React, { Component } from "react";
import { render } from "react-dom";
import PropTypes from 'prop-types'; // 定义组件属性类型校验
import "./index.css";
import ListItems from "./ListItems";
import Header from "./Header";
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
todoItem: "",
items: ["吃苹果","吃香蕉","喝奶茶"]
};
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
// 输入框onChange事件
onChange(event) {
this.setState({
todoItem: event.target.value
});
}
// 表单提交按钮单击事件
onSubmit(event) {
event.preventDefault();
this.setState({
todoItem: "",
items: [
...this.state.items,
this.state.todoItem
]
});
}
render() {
return (
<div className="container">
<Header title="TodoList"/>
<form className="form-wrap" onSubmit={this.onSubmit}>
<input value={this.state.todoItem} onChange={this.onChange} />
<button>Submit</button>
</form>
<ListItems items={this.state.items} />
</div>
);
}
}
App.propTypes = {
items: PropTypes.array,
todoItem: PropTypes.string,
onChange: PropTypes.func,
onSubmit: PropTypes.func
};
render(
<App />,
document.getElementById("app")
);
本例最终的展示效果如图2.4所示。
项目源码可在GitHub进行下载 ,地址是https://github.com/khno/react-comonent-todolist 。