在这篇文章中,我会介绍 React 各个生命周期在实际项目中的应用场景,并给出代码示例。
React 生命周期图谱:https://projects.wojtekmaj.pl...
挂载阶段(Mounting)
组件实例创建并插入到 DOM 中。这个阶段调用生命周期函数顺序依次是: coustructor()、static getDerivedStateFromProps()、render()、componentDidMount()。
更新阶段(Updating)
组件的 state 或者 props 发生变化时会触发更新。这个阶段调用生命周期函数顺序依次是:static getDerivedStateFromProps()、shouldComponentUpdate()、render()、getSnapshotBeforeUpdate()、componentDidUpdate()。
卸载阶段(UnMounting)
组件从 DOM 中移除时,会调用 componentWillUnmount() 方法。
错误处理(Error boundaries)
当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:static getDerivedStateFromError()、componentDidCatch()。
render()
注意⚠️:
- render() 函数是 class 组件中唯一必须实现的方法;
- render 函数的返回值:React 元素、数组、Fragments、Portals、字符串或数值、布尔值或 null;
- render 应为纯函数,并且不会与浏览器产生交互,这样使组件更加容易理解。
constructor(props)
调用时间:
React 组件在挂载之前,会调用它的构造函数。
使用场景:
- 初始化 state
- 给事件处理函数绑定实例
class Test extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
const { counter } = this.state;
this.setState({ counter: counter + 1 });
}
render() {
return (
{this.state.counter}
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
注意⚠️:
- 不要在构造函数中调用 setState 方法;
- 在为 React.Component 子类实现构造函数时,应在其他语句之前调用 super(props);
- 避免在构造函数中引入副作用或者订阅;
- 避免将 props 的值赋值给 state(why?)。
componentDidMount()
调用时间:
React 组件挂载后(插入DOM 树中)立即调用。
使用场景:
- 依赖于 DOM 节点的初始化操作;
- 通过网络请求获取数据;
- 添加订阅;
注意⚠️:
在此处如果调用 setState(),将会触发渲染,这个渲染将会发生在浏览器更新屏幕之前,因此用户不会有感知,但是要谨慎使用该模式,因为会导致性能问题。 如果你的渲染依赖于 DOM 节点的位置和大小,比如 Modal 和 Tooltip ,则可以使用该模式。
componentDidUpdate(prevProps, prevState, snapshot)
调用时间:
React 组件更新后立即调用,首次渲染不会调用此方法。
使用场景:
- 在组件更新后,在此处对 DOM 进行操作;
- 对更新前后的 props 进行比较,如果有变化,执行操作(比如网络请求、执行 setState() 等);
componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props):
if (this.props.id !== prevProps.id) {
this.fetchData(this.props.id);
}
}
注意⚠️:
- 不要将 props “镜像”给 state(why?)
- 如果组件实现了 getSnapshotBeforeUpdate() 生命周期,则它的返回值将作为 componentDidUpdate 的第三个参数(snapshot)传入,否则,第三个参数将为 undefined;
- 如果 shouldComponentUpdate() 返回值为 false,则不会调用 componentDidUpdate()。
componentWillUnmount()
调用时间:
React 组件卸载或销毁之前调用
使用场景:
- 清除 timer(setInterval());
- 取消网络请求;
- 取消在 componentDidMount() 中创建的订阅。
注意⚠️:
在该生命周期中不要调用 setState(),因为组件卸载后,不会重新渲染。
shouldComponentUpdate(nextProps, nextState)
调用时间:
当 state 或者 props 发生变化时,shouldComponentUpdate() 将会在 render() 之前调用,首次渲染或者使用 forceUpdate() 时不会调用。
使用场景:
可以通过手写覆盖 shouldComponentUpdate() 方法对 React 组件进行性能优化,但是大部分情况下,可以通过继承 React.pureComponent 代替手写 shouldComponentUpdate(),React.pureComponent 实现了 shouldComponentUpdate() 方法(用当前和之前的 props 和 state 进行浅比较)。
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}
shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}
render() {
return (
);
}
}
注意⚠️:
- 此方法默认返回true,其遵循默认行为——“state 或者 props 发生变化时,组件重新渲染”;如果返回 false,则组件不会重新渲染,大部分情况下,你该遵循默认行为。
- 此方法为了性能优化而存在,不要试图使用此方法阻止渲染,因为这可能产生 bug;
- 如果你一定要手动编写此方法,需要对 this.props 和 nextProps、this.state 和 nextState 进行比较。
- 不建议在 shouldComponentUpdate() 中使用 JSON.stringfy() 或者进行深层次比较,这样会极其的耗费性能。
static getDerivedStateFromProps(props,state)
调用时间:
render() 方法之前调用,在初始挂载和后续更新都会调用。
使用场景:
适用于罕用例,即 state 的值在任何时候都取决于 props。
// 例子1:
class Example extends React.Component {
state = {
isScrollingDown: false,
lastRow: null
};
static getDerivedStateFromProps(props, state) {
if (props.currentRow !== state.lastRow) {
return {
isScrollingDown: props.currentRow > state.lastRow,
lastRow: props.currentRow
};
}
// 返回 null 表示无需更新 state。
return null;
}
}
// 例子2:根据 props 获取 externalData
class ExampleComponent extends React.Component {
state = {
externalData: null,
};
static getDerivedStateFromProps(props, state) {
// 保存 prevId 在 state 中,以便我们在 props 变化时进行对比。
// 清除之前加载的数据(这样我们就不会渲染旧的内容)。
if (props.id !== state.prevId) {
return {
externalData: null,
prevId: props.id
};
}
// 无需更新 state
return null;
}
componentDidMount() {
this._loadAsyncData(this.props.id);
}
componentDidUpdate(prevProps, prevState) {
if (this.state.externalData === null) {
this._loadAsyncData(this.props.id);
}
}
componentWillUnmount() {
if (this._asyncRequest) {
this._asyncRequest.cancel();
}
}
render() {
if (this.state.externalData === null) {
// 渲染加载状态 ...
} else {
// 渲染真实 UI ...
}
}
_loadAsyncData(id) {
this._asyncRequest = loadMyAsyncData(id).then(
externalData => {
this._asyncRequest = null;
this.setState({externalData});
}
);
}
}
注意⚠️:
派生状态会导致代码冗余,并使组件难以维护,可以使用如下替换方案:
(1)memoization 模式:如果只是为了缓存基于当前 props 计算之后的结果的话,没有必要使用 getDerivedStateFromProps(),因为管理 state 的复杂度会随着需要管理的属性的增多而越来越庞大,比如,如果我们想在组件 state 里添加第二个派生 state,那就需要写两份跟踪变化的逻辑。为了让代码变得简单和易于管理,可以尝试使用 memoization。
// *******************************************************
// 注意:这个例子不是建议的方法。
// 下面的例子才是建议的方法。
// *******************************************************
static getDerivedStateFromProps(props, state) {
// 列表变化或者过滤文本变化时都重新过滤。
// 注意我们要存储 prevFilterText 和 prevPropsList 来检测变化。
if (
props.list !== state.prevPropsList ||
state.prevFilterText !== state.filterText
) {
return {
prevPropsList: props.list,
prevFilterText: state.filterText,
filteredList: props.list.filter(item => item.text.includes(state.filterText))
};
}
return null;
}
// 使用 memoization
import memoize from "memoize-one";
class Example extends Component {
// state 只需要保存当前的 filter 值:
state = { filterText: "" };
// 在 list 或者 filter 变化时,重新运行 filter:
filter = memoize(
(list, filterText) => list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// 计算最新的过滤后的 list。
// 如果和上次 render 参数一样,`memoize-one` 会重复使用上一次的值。
const filteredList = this.filter(this.props.list, this.state.filterText);
return (
{filteredList.map(item => - {item.text}
)}
);
}
}
(2)使用完全受控的组件:
function EmailInput(props) {
return ;
}
(3)使用有 key 的非可控组件(当 key 变化的时候,React 就会创建一个新的而不是一个既有的组件),大部分时候,这是处理重置 state 的最好的办法。
class EmailInput extends Component {
state = { email: this.props.defaultEmail };
handleChange = event => {
this.setState({ email: event.target.value });
};
render() {
return ;
}
}
但是在某些情况下,key 可能不起作用,这时候可以使用 getDerivedStateFromProps() 来观察属性变化。
class EmailInput extends Component {
state = {
email: this.props.defaultEmail,
prevPropsUserID: this.props.userID
};
static getDerivedStateFromProps(props, state) {
// 只要当前 user 变化,
// 重置所有跟 user 相关的状态。
// 这个例子中,只有 email 和 user 相关。
if (props.userID !== state.prevPropsUserID) {
return {
prevPropsUserID: props.userID,
email: props.defaultEmail
};
}
return null;
}
// ...
}
getSnapshotBeforeUpdate(prevProps, prevState)
调用时间:
在最近一次渲染输出(提交到 DOM 节点)之前调用。
使用场景:
组件在发生更改之前可以从 DOM 中获取一些信息,这个方法可能用在 UI 处理中,例如滚动位置。
class ScrollingList extends React.Component {
constructor(props) {
super(props);
this.listRef = React.createRef();
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 我们是否在 list 中添加新的 items ?
// 捕获滚动位置以便我们稍后调整滚动位置。
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 有值,说明我们刚刚添加了新的 items,
// 调整滚动位置使得这些新 items 不会将旧的 items 推出视图。
//(这里的 snapshot 是 getSnapshotBeforeUpdate 的返回值)
if (snapshot !== null) {
const list = this.listRef.current;
list.scrollTop = list.scrollHeight - snapshot;
}
}
render() {
return (
{/* ...contents... */}
);
}
}
static getDerivedStateFromError(error)
调用时间:
该方法在组件抛出错误时被调用。它将抛出的错误作为参数,并且返回一个值以更新 state。
使用场景:
显示降级 UI
注意⚠️:
该方法会在渲染阶段调用,因此不允许出现副作用,如遇此类情况,可以用 componentDidCatch()。
componentDidCatch(error, info)
调用时间:
该方法在“提交”的阶段被调用。因此允许执行副作用。
使用场景:
用于记录错误,该方法的第二个参数包含有关引发组件错误的栈信息。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显示降级 UI
return { error: true };
}
componentDidCatch(error, errorInfo) {
this.setState({ errorInfo })
}
render() {
if (this.state.errorInfo) {
return (
Something went wrong.
{this.state.error && this.state.error.toString()}
{this.state.errorInfo.componentStack}
);
}
return this.props.children;
}
}
class BuggyCounter extends React.Component {
constructor(props) {
super(props);
this.state = { counter: 0 };
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState(({counter}) => ({ counter: counter + 1 }));
}
render() {
if (this.state.counter === 5) {
// 此处渲染会产生错误
return [1,2,3];
}
return {this.state.counter}
;
}
}
function App() {
return (
This is an example of error boundaries in React 16.
Click on the numbers to increase the counters.
The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.
These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.
These two counters are each inside of their own error boundary. So if one crashes, the other is not affected.
);
}