@qinyun
2019-01-31T16:24:22.000000Z
字数 11272
阅读 1609
未分类
什么是生命周期方法?新的React16+生命周期方法是怎样的?你该如何直观地理解它们,以及为什么它们很有用?
React组件都有自己的阶段。
如果要你“构建一个Hello World组件”,我相信你会这么做:
class HelloWorld extends React.Component {
render() {
return <h1> Hello World </h1>
}
}
在客户端渲染这个组件时,你最终可能会看到如下的视图:
在呈现这个视图之前,这个组件经历了几个阶段。这些阶段通常称为组件生命周期。
对于人类而言,我们会经历小孩、成人、老人阶段。而对于React组件而言,我们有挂载、更新和卸载阶段。
巧合的是,挂载一个组件就像将一个新生婴儿带到这个世界。这是组件第一次拥有了生命。组件正是在这个阶段被创建,然后被插入到DOM中。
这是组件经历的第一个阶段——挂载阶段。
但它并不会就这样结束了。React组件会“成长”,或者说组件会经历更新阶段。
如果React组件不经历更新阶段,它们将保持被创建时的状态。
大部分组件会被更新——无论是通过修改state还是props,也就是经历更新阶段。
组件经历的最后一个阶段是卸载阶段。
在这个阶段,组件会“死亡”。用React术语来描述,就是指从DOM中移除组件。
这些就是你需要了解的有关组件生命周期的一切。
对了,React组件还需要经历另一个阶段。有时候代码会无法运行或者某处出现了错误,这个时候组件正在经历错误处理阶段,就像人类去看医生。
现在,你了解了React组件的四个基本阶段或者说生命周期。
1.挂载——组件在这个阶段被创建然后被插入到DOM中;
2.更新——React组件“成长”;
3.卸载——最后阶段;
4.错误处理——有时候代码无法运行或某处出现了错误。
注意:React组件可能不会经历所有阶段。一个组件有可能在挂载后立即就被卸载——没有更新或错误处理。
了解组件经历的各个阶段只是整个等式的一部分,另一部分是了解每个阶段所对应的方法。
这些方法就是众所周知的组件生命周期方法。
让我们来看看这4个阶段所对应的方法。
我们先来看一下挂载阶段的方法。
挂载阶段是指从组件被创建到被插入DOM的阶段。
这个阶段会调用以下几个方法(按顺序描述)。
这是给组件“带来生命”时调用的第一个方法。
在将组件挂载到DOM之前会调用constructor方法。
通常,你会在constructor方法中初始化state和绑定事件处理程序。
这是一个简单的例子:
const MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = {
points: 0
}
this.handlePoints = this.handlePoints.bind(this)
}
}
我相信你已经很熟悉这个方法了,所以我不打算进一步再做解释。
需要注意的是,这是第一个被调用的方法——在组件被挂载到DOM之前。
在解释这个生命周期方法之前,我先说明如何使用这个方法。
这个方法的基本结构如下所示:
const MyComponent extends React.Component {
...
static getDerivedStateFromProps() {
//do stuff here
}
}
这个方法以props和state作为参数:
...
static getDerivedStateFromProps(props, state) {
//do stuff here
}
...
你可以返回一个用于更新组件状态的对象:
...
static getDerivedStateFromProps(props, state) {
return {
points: 200 // update state with this
}
}
...
或者返回null,不进行更新:
...
static getDerivedStateFromProps(props, state) {
return null
}
...
你可能会想,这个生命周期方法很重要吗?它是很少使用的生命周期方法之一,但它在某些情况下会派上用场。
请记住,这个方法在组件被初始挂载到DOM之前调用。
下面是一个简单的例子:
假设有一个简单的组件,用于呈现足球队的得分。
得分被保存在组件的state对象中:
class App extends Component {
state = {
points: 10
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
You've scored {this.state.points} points.
</p>
</header>
</div>
);
}
}
结果如下所示:
源代码可以在GitHub上获得:https://github.com/ohansemmanuel/points
假设你像下面这样在static getDerivedStateFromProps方法中放入其他分数,那么呈现的分数是多少?
class App extends Component {
state = {
points: 10
}
// *******
// NB: Not the recommended way to use this method. Just an example. Unconditionally overriding state here is generally considered a bad idea
// ********
static getDerivedStateFromProps(props, state) {
return {
points: 1000
}
}
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
You've scored {this.state.points} points.
</p>
</header>
</div>
);
}
}
现在我们有了static getDerivedStateFromProps组件生命周期方法。在将组件挂载到DOM之前这个方法会被调用。通过返回一个对象,我们可以在组件被渲染之前更新它的状态。
我们将看到:
1000来自static getDerivedStateFromProps方法的状态更新。
当然,这个例子主要是出于演示的目的,static getDerivedStateFromProps方法不应该被这么用。我这么做只是为了让你先了解这些基础知识。
我们可以使用这个生命周期方法来更新状态,但并不意味着必须这样做。static getDerivedStateFromProps方法有它特定的应用场景。
那么什么时候应该使用static getDerivedStateFromProps方法呢?
方法名getDerivedStateFromProps包含五个不同的单词:“Get Fromived State From Props”。
顾名思义,这个方法允许组件基于props的变更来更新其内部状态。
此外,以这种方式获得的组件状态被称为派生状态。
根据经验,应该谨慎使用派生状态,因为如果你不确定自己在做什么,很可能会向应用程序引入潜在的错误。
在调用static getDerivedStateFromProps方法之后,下一个生命周期方法是render:
class MyComponent extends React.Component {
// render is the only required method for a class component
render() {
return <h1> Hurray! </h1>
}
}
如果要渲染DOM中的元素,可以在render方法中编写代码,即返回一些JSX。
你还可以返回纯字符串和数字,如下所示:
class MyComponent extends React.Component {
render() {
return "Hurray"
}
}
或者返回数组和片段,如下所示:
class MyComponent extends React.Component {
render() {
return [
<div key="1">Hello</div>,
<div key="2" >World</div>
];
}
}
class MyComponent extends React.Component {
render() {
return <React.Fragment>
<div>Hello</div>
<div>World</div>
</React.Fragment>
}
}
如果你不想渲染任何内容,可以在render方法中返回一个布尔值或null:
class MyComponent extends React.Component {
render() {
return null
}
}
class MyComponent extends React.Component {
// guess what's returned here?
render() {
return (2 + 2 === 5) && <div>Hello World</div>;
}
}
你还可以从render方法返回一个portal:
class MyComponent extends React.Component {
render() {
return createPortal(this.props.children, document.querySelector("body"));
}
}
关于render方法的一个重要注意事项是,不要在函数中调用setState或者与外部API发生交互。
在调用render后,组件被挂载到DOM,并调用componentDidMount方法。
在将组件被挂载到DOM之后会立即调用这个函数。
有时候你需要在组件挂载后立即从组件树中获取DOM节点,这个时候就可以调用这个组件生命周期方法。
例如,你可能有一个模态窗口,并希望在特定DOM元素中渲染模态窗口的内容,你可以这么做:
class ModalContent extends React.Component {
el = document.createElement("section");
componentDidMount() {
document.querySelector("body).appendChild(this.el);
}
// using a portal, the content of the modal will be rendered in the DOM element attached to the DOM in the componentDidMount method.
}
如果你希望在组件被挂载到DOM后立即发出网络请求,可以在这个方法里进行:
componentDidMount() {
this.fetchListOfTweets() // where fetchListOfTweets initiates a netowrk request to fetch a certain list of tweets.
}
你还可以设置订阅,例如计时器:
// e.g requestAnimationFrame
componentDidMount() {
window.requestAnimationFrame(this._updateCountdown);
}
// e.g event listeners
componentDidMount() {
el.addEventListener()
}
只需要确保在卸载组件时取消订阅,我们将在讨论componentWillUnmount生命周期方法时介绍更详细的内容。
挂载阶段基本上就是这样了,现在让我们来看看组件经历的下一个阶段——更新阶段。
每当更改React组件的state或props时,组件都会被重新渲染。简单地说,就是组件被更新。这就是组件生命周期的更新阶段。
那么在更新组件时会调用哪些生命周期方法?
首先,还会调用static getDerivedStateFromProps方法。这是第一个被调用的方法。因为之前已经介绍过这个方法,所以这里不再解释。
需要注意的是,在挂载和更新阶段都会调用这个方法。
在调用static getDerivedStateFromProps方法之后,接下来会调用nextComponentUpdate方法。
默认情况下,或者在大多数情况下,在state或props发生变更时会重新渲染组件。不过,你也可以控制这种行为。
你可以在这个方法中返回一个布尔值——true或false,用于控制是否重新渲染组件。
这个生命周期方法主要用于优化性能。不过,如果state和props没有发生变更,不希望组件重新渲染,你也可以使用内置的PureComponent。
在调用shouldComponentUpdate方法后,会立即调用render——具体取决于shouldComponentUpdate返回的值,默认为true。
在调用render方法之后,接下来会调用getSnapshotBeforeUpdatelifcycle方法。
你不一定会用到这个生命周期方法,但在某些特殊情况下它可能会派上用场,特别是当你需要在DOM更新后从中获取一些信息。
这里需要注意的是,getSnapshotBeforeUpdate方法从DOM获得的值将引用DOM更新之前的值,即使之前调用了render方法。
我们以使用git作为类比。
在编写代码时,你会在将代码推送到代码库之前暂存它们。
假设在将变更推送到DOM之前调用了render函数来暂存变更。因此,在实际更新DOM之前,getSnapshotBeforeUpdate获得的信息指向了DOM更新之前的信息。
对DOM的更新可能是异步的,但getSnapshotBeforeUpdate生命周期方法在更新DOM之前立即被调用。
如果你还是不太明白,我再举一个例子。
聊天应用程序是这个生命周期方法的一个典型应用场景。
我已经为之前的示例应用程序添加了聊天窗格。
可以看到右侧的窗格吗?
聊天窗格的实现非常简单,你可能已经想到了。在App组件中有一个带有Chats组件的无序列表:
<ul className="chat-thread">
<Chats chatList={this.state.chatList} />
</ul>
Chats组件用于渲染聊天列表,为此,它需要一个chatList prop。基本上它就是一个数组,一个包含3个字符串的数组:[“Hey”, “Hello”, “Hi”]。
Chats组件的实现如下:
class Chats extends Component {
render() {
return (
<React.Fragment>
{this.props.chatList.map((chat, i) => (
<li key={i} className="chat-bubble">
{chat}
</li>
))}
</React.Fragment>
);
}
}
它只是通过映射chatList prop并渲染出一个列表项,而该列表项的样式看起来像气泡。
还有一个东西,在聊天窗格顶部有一个“Add Chat”按钮。
看到聊天窗格顶部的按钮了吗?
单击这个按钮将会添加新的聊天文本“Hello”,如下所示:
与大多数聊天应用程序一样,这里有一个问题:每当消息数量超过聊天窗口的高度时,预期的行为应该是自动向下滚动聊天窗格,以便看到最新的聊天消息。大现在的情况并非如此。
让我们看看如何使用getSnapshotBeforeUpdate生命周期方法来解决这个问题。
在调用getSnapshotBeforeUpdate方法时,需要将之前的props和state作为参数传给它。
我们可以使用prevProps和prevState参数,如下所示:
getSnapshotBeforeUpdate(prevProps, prevState) {
}
你可以让这个方法返回一个值或null:
getSnapshotBeforeUpdate(prevProps, prevState) {
return value || null // where 'value' is a valid JavaScript value
}
无论这个方法返回什么值,都会被传给另一个生命周期方法。
getSnapshotBeforeUpdate生命周期方法本身不会起什么作用,它需要与componentDidUpdate生命周期方法结合在一起使用。
你先记住这个,让我们来看一下componentDidUpdate生命周期方法。
在调用getSnapshotBeforeUpdate之后会调用这个生命周期方法。与getSnapshotBeforeUpdate方法一样,它接收之前的props和state作为参数:
componentDidUpdate(prevProps, prevState) {
}
但这并不是全部。
无论从getSnapshotBeforeUpdate生命周期方法返回什么值,返回值都将被作为第三个参数传给componentDidUpdate方法。
我们姑且把返回值叫作snapshot,所以:
componentDidUpdate(prevProps, prevState, snapshot) {
}
有了这些,接下来让我们来解决聊天自动滚动位置的问题。
要解决这个问题,我需要提醒(或教导)你一些DOM几何学知识。
下面是保持聊天窗格滚动位置所需的代码:
getSnapshotBeforeUpdate(prevProps, prevState) {
if (this.state.chatList > prevState.chatList) {
const chatThreadRef = this.chatThreadRef.current;
return chatThreadRef.scrollHeight - chatThreadRef.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
const chatThreadRef = this.chatThreadRef.current;
chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;
}
}
这是聊天窗口:
下图突出显示了保存聊天消息的实际区域(无序列表ul)。
我们在ul中添加了React Ref:
<ul className="chat-thread" ref={this.chatThreadRef}>
...
</ul>
首先,因为getSnapshotBeforeUpdate可以通过任意数量的props或state更新来触发更新,我们将通过一个条件来判断是否有新的聊天消息:
getSnapshotBeforeUpdate(prevProps, prevState) {
if (this.state.chatList > prevState.chatList) {
// write logic here
}
}
getSnapshotBeforeUpdate必须返回一个值。如果没有添加新聊天消息,就返回null:
getSnapshotBeforeUpdate(prevProps, prevState) {
if (this.state.chatList > prevState.chatList) {
// write logic here
}
return null
}
现在看一下getSnapshotBeforeUpdate方法的完整代码:
getSnapshotBeforeUpdate(prevProps, prevState) {
if (this.state.chatList > prevState.chatList) {
const chatThreadRef = this.chatThreadRef.current;
return chatThreadRef.scrollHeight - chatThreadRef.scrollTop;
}
return null;
}
我们先考虑一种情况,即所有聊天消息的高度不超过聊天窗格的高度。
表达式chatThreadRef.scrollHeight - chatThreadRef.scrollTop等同于chatThreadRef.scrollHeight - 0。
这个表达式的值将等于聊天窗格的scrollHeight——在将新消息插入DOM之前的高度。
之前我们已经解释过,从getSnapshotBeforeUpdate方法返回的值将作为第三个参数传给componentDidUpdate方法,也就是snapshot:
componentDidUpdate(prevProps, prevState, snapshot) {
}
这个值是更新DOM之前的scrollHeight。
componentDidUpdate方法有以下这些代码,但它们有什么作用呢?
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
const chatThreadRef = this.chatThreadRef.current;
chatThreadRef.scrollTop = chatThreadRef.scrollHeight - snapshot;
}
}
实际上,我们以编程方式从上到下垂直滚动窗格,距离等于chatThreadRef.scrollHeight - snapshot;。
由于snapshot是指更新前的scrollHeight,上述的表达式将返回新聊天消息的高度,以及由于更新而导致的任何其他相关高度。请看下图:
当整个聊天窗格高度被消息占满(并且已经向上滚动一点)时,getSnapshotBeforeUpdate方法返回的snapshot值将等于聊天窗格的实际高度。
componentDidUpdate将scrollTop值设置为额外消息高度的总和,这正是我们想要的。
在组件卸载阶段会调用下面这个方法。
在卸载和销毁组件之前会调用componentWillUnmount生命周期方法。这是进行资源清理最理想的地方,例如清除计时器、取消网络请求或清理在componentDidMount()中创建的任何订阅,如下所示:
// e.g add event listener
componentDidMount() {
el.addEventListener()
}
// e.g remove event listener
componentWillUnmount() {
el.removeEventListener()
}
有时候组件会出现问题,会抛出错误。当后代组件(即组件下面的组件)抛出错误时,将调用下面的方法。
让我们实现一个简单的组件来捕获演示应用程序中的错误。为此,我们将创建一个叫作ErrorBoundary的新组件。
这是最基本的实现:
import React, { Component } from 'react';
class ErrorBoundary extends Component {
state = {};
render() {
return null;
}
}
export default ErrorBoundary;
当后代组件抛出错误时,首先会调用这个方法,并将抛出的错误作为参数。
无论这个方法返回什么值,都将用于更新组件的状态。
让ErrorBoundary组件使用这个生命周期方法:
import React, { Component } from "react";
class ErrorBoundary extends Component {
state = {};
static getDerivedStateFromError(error) {
console.log(`Error log from getDerivedStateFromError: ${error}`);
return { hasError: true };
}
render() {
return null;
}
}
export default ErrorBoundary;
现在,只要后代组件抛出错误,错误就会被记录到控制台,并且getDerivedStateFromError方法会返回一个对象,这个对象将用于更新ErrorBoundary组件的状态。
在后代组件抛出错误之后,也会调用componentDidCatch方法。除了抛出的错误之外,还会有另一个参数,这个参数包含了有关错误的更多信息:
componentDidCatch(error, info) {
}
在这个方法中,你可以将收到的error或info发送到外部日志记录服务。与getDerivedStateFromError不同,componentDidCatch允许包含会产生副作用的代码:
componentDidCatch(error, info) {
logToExternalService(error, info) // this is allowed.
//Where logToExternalService may make an API call.
}
让ErrorBoundary组件使用这个生命周期方法:
import React, { Component } from "react";
class ErrorBoundary extends Component {
state = { hasError: false };
static getDerivedStateFromError(error) {
console.log(`Error log from getDerivedStateFromError: ${error}`);
return { hasError: true };
}
componentDidCatch(error, info) {
console.log(`Error log from componentDidCatch: ${error}`);
console.log(info);
}
render() {
return null
}
}
export default ErrorBoundary;
此外,由于ErrorBoundary只能捕捉后代组件抛出的错误,因此我们将让组件渲染传进来的Children,或者在出现错误时呈现默认的错误UI:
...
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}