本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情
生命周期流程
虽然在16.8
版本出了hook后类组件的应用被函数组件逐渐取代,但是在工作中我们还是有很多现行的类组件代码项目需要维护。因此我们还是有必要理解类组件的生命周期和避免生命周期中的坑。(本文底部有使用范例)
我们可以在线查看react类组件生命周期,我们知道了16.3
版本后16.4
之后版本的区别主要在于getDerivedStateFromProps
这个函数。
挂载时
上面示例初始化页面时输出:
组件初始挂载时依次触发以下生命周期函数:
1.constructor
构造函数
通常在构造函数中处理两种情况:①:初始化组件内部state。②为事件处理函数绑定实例:
constructor(props) {
super(props);
// 不要在这里调用 this.setState()
this.state = {
counter: 0,
name: this.props.name // 不要使用props.name赋值
};
this.handleClick = this.handleClick.bind(this);
}
此处要注意几个情况:
- 在constructor中先调用
super(props)
,防止在下面语句中使用this.props
出现未定义的错误 - 在使用props时,直接使用
this.props
,若在constructor中使用props赋值,当props更新时,不会影响state。 - 不要再constructor中调用setState,可以直接
this.state = ...
赋值
2.static getDerivedStateFromProps
static getDerivedStateFromProps(props, state) {
// 只要name变化,修改当前组件关于name的state值
if (props.name !== state.name) {
return {
name: props.name
};
}
return null;
}
-
getDerivedStateFromProps
会在调用 render 前调用。 -
该方法需要return一个对象来更新state。这个对象并不会完整替换掉state,更新后,原有属性与新属性是共存的。如果返回null则不触发更新。
-
初始挂载和更新时都会被调用。
-
该方法是静态方法,该方法内部无法访问this。
-
派生状态会导致代码冗余,并使组件难以维护。某些情况下我们可以使用以下方法替代
getDerivedStateFromProps
:componentDidUpdate
:在需要执行副作用来响应 props 中的更改(例如,数据提取或动画)是代替使用memoization
:依赖prop更改时重新计算某些数据时- 使用完全受控组件:在 prop 更改时“重置”某些 state,请考虑使组件完全受控或使用
key
使组件完全不受控 代替。
3.render()
渲染函数
当 render
被调用时,它会检查 this.props
和 this.state
的变化并返回以下类型之一:
- React元素:通过JSX渲染成dom节点
- 数组或fragments:可以使render返回多个元素
- Portals:可以渲染子节点到不同的 DOM 子树中
- 字符串/数值:渲染为文本节点
- 布尔值/null:什么都不渲染
4.componentDidMount()
组件挂载后调用该函数。我们通常在componentDidMount
中:①发起网络请求获取页面数据。②添加订阅/添加定时器任务等,同时需要在componentWillUnmount
中取消订阅。
对比
- 两个版本相比,少了
componentWillMount
生命周期函数,多了getDerivedStateFromPorps
函数 componentWillMount
被废弃的原因:①setState:该方法再render之前调用,在此处ajax请求到数据并调用setState发生在render之前,则会导致setState无效,不会触发额外渲染。 ②Fiber原因:由于React16版本中render()
之前的生命周期函数可能会被打断,所以可能会造成这些生命周期被多次执行,容易出现问题。- 新增
getDerivedStateFromPorps
的主要原因是想要替换调componentWillReceiveProps
这个生命周期函数。初始化时也会触发getDerivedStateFromPorps
是为了保障这个生命周期的纯洁性,直接从命名层面约束了它的用途(从 Props 里派生 State) - 另外
react16
中的render的返回内容新增了 数组和字符串
更新时
上面示例,点击按钮组件更新时依次触发:
1.getDerivedStateFromProps()
组件更新:由父组件触发(16.3版本)
2.shoudComponentUpdate()
组件更新:组件自身的更新触发
- shouldComponentUpdate返回一个布尔值,用来判断props或state中某些值改变时是否要触发render更新。
- 默认情况下state变化会触发重新渲染,另外返回
false
并不会阻止子组件在 state 更改时重新渲染。 - 首次渲染或使用
forceUpdate()
时不会调用该方法。 - 此方法常作为性能优化方式存在。如果不希望props和state进行深层比较,可以使用
PureComponent
3.render()
4. getSnapshotBeforeUpdate()
- 该方法执行时机是render函数之后,真实dom更新之前。
- 可以在此时获取dom更新之前的一些信息,比如滚动位置之类的。
- 此方法可以返回值作为参数传递给componentDidUpdate。
- 该方法不常见,可在一些类似需要特殊方式处理滚动位置的聊天线程等场景中使用。
5.componentDidUpdate()
- 该方法在更新后立即调用
- 组件更新后,可以在这里对dom操作
- 也可以在这里判断props的变化进行网络请求
componentDidUpdate(prevProps) {
// 典型用法(不要忘记比较 props):
if (this.props.name !== prevProps.name) {
this.fetchData(this.props.name);
}
}
对比
-
16.3与16.4之后的版本中的
getDerivedStateFromProps
做了微调。在更新流程上:16.3版本中只有父组件的更新会触发该生命周期函数;16.4中,除了父组件更新外,setState
和forceUpdate
也会触发该函数。 -
使用
getDerivedStateFromProps
替换componentWillReceiveProps
的原因:①父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法。②此处使用setState,增加组件的重绘;若判断失效,可能导致无限重绘。 -
getDerivedStateFromProps
并不完全等同于componentWillReceiveProps
。替换componentWillReceiveProps的方法getDerivedStateFromProps
生命周期替代componentWillReceiveProps
的背后,是 React 16 在强制推行“只用getDerivedStateFromProps
来完成props 到 state
的映射”这一最佳实践。确保生命周期函数的行为更加可控可预测,从根源上帮开发者避免不合理的编程方式,避免生命周期的滥用;同时,也是在为新的 Fiber 架构铺路。 -
废弃的
componentWillUpdate
与新增的getSnapshotBeforeUpdate
:①componentWillUpdate
中不能调用setState,也不应进行任何操作(如dispatch等)。②getSnapshotBeforeUpdate
可以返回值与componentDidUpdate
通信。这个阶段可以同时获取到更新前的真实 DOM 和更新前后的state&props
信息。此外对于获取更新前滚动位置的场景,我们可以两者结合使用。值得一提的是,这个生命周期的设计初衷,是为了“与
componentDidUpdate
一起,涵盖过时的componentWillUpdate
的所有用例”(引用自 React 官网)。getSnapshotBeforeUpdate
要想发挥作用,离不开componentDidUpdate
的配合那么换个角度想想,为什么
componentWillUpdate
就非死不可呢?说到底,还是因为它“挡了 Fiber 的路”。
卸载时
componentWillUnmount()
- 在组件销毁之前调用
- 在此方法中可以取消在componentDidMount中的一些订阅,清除timer,取消网络请求等
- 该方法中不应再调用setState,因为此处已经是组件卸载了,不会再触发重新渲染。
- 该方法16版本与15版本一样
生命周期中容易遇到的坑
有以下几种情况容易造成生命周期的坑
getDerivedStateFromProps
容易编写反模式代码,使受控组件与非受控组件区分模糊componentWillMount
在 React 中已被标记弃用,不推荐使用,主要原因是新的异步渲染架构会导致它被多次调用
。所以网络请求及事件绑定代码应移至componentDidMount
中。componentWillReceiveProps
同样被标记弃用,被getDerivedStateFromProps
所取代,主要原因是性能问题shouldComponentUpdate
通过返回true
或者false
来确定是否需要触发新的渲染。主要用于性能优化componentWillUpdate
同样是由于新的异步渲染机制,而被标记废弃,不推荐使用,原先的逻辑可结合getSnapshotBeforeUpdate
与componentDidUpdate
改造使用。- 如果在
componentWillUnmount
函数中忘记解除事件绑定,取消定时器等清理操作,容易引发 bug - 如果没有添加错误边界处理,当渲染发生异常时,用户将会看到一个无法操作的白屏,所以一定要添加
React生命周期的两个阶段
Fiber 是 React 16 对 React 核心算法的一次重写。你只需要 get 到这一个点:
Fiber 会使原本同步的渲染过程变成异步的
。
-
在 React 16 之前,每当我们触发一次组件的更新,React 都会构建一棵新的虚拟 DOM 树,通过与上一次的虚拟 DOM 树进行 diff,实现对 DOM 的定向更新。这个过程,是一个递归的过程。
同步渲染的递归调用栈是非常深的,只有最底层的调用返回了,整个渲染过程才会开始逐层返回
。这个漫长且不可打断的更新过程,将会带来用户体验层面的巨大风险:同步渲染一旦开始,便会牢牢抓住主线程不放,直到递归彻底完成
。在这个过程中,浏览器没有办法处理任何渲染之外的事情,会进入一种无法处理用户交互的状态。因此若渲染时间稍微长一点,页面就会面临卡顿甚至卡死的风险。 -
React 16 引入的 Fiber 架构,恰好能够解决掉这个风险:
Fiber 会将一个大的更新任务拆解为许多个小任务
。每当执行完一个小任务时,渲染线程都会把主线程交回去
,看看有没有优先级更高的工作要处理,确保不会出现其他任务被“饿死”的情况,进而避免同步渲染带来的卡顿。在这个过程中,渲染线程不再“一去不回头”,而是可以被打断的,这就是所谓的“异步渲染”。
换个角度看生命周期工作流
Fiber 架构的重要特征就是可以被打断的异步渲染模式。但这个“打断”是有原则的,根据“能否被打断”这一标准,React 16 的生命周期被划分为了 render 和 commit 两个阶段
,而 commit 阶段又被细分为了 pre-commit 和 commit
。每个阶段所涵盖的生命周期如即本文顶图,此处不再贴图
我们先来看下三个阶段各自有哪些特征
render 阶段
:纯净且没有副作用,可能会被 React 暂停、终止或重新启动。pre-commit 阶段
:可以读取 DOM。commit 阶段
:可以使用 DOM,运行副作用,安排更新。
总的来说,render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。
为什么这样设计呢?简单来说,
由于 render 阶段的操作对用户来说其实是“不可见”的,所以就算打断再重启,对用户来说也是零感知
。而commit 阶段的操作则涉及真实 DOM 的渲染
,所以这个过程必须用同步渲染来求稳
。
使用范例:整个生命周期
import React from "react";
import ReactDOM from "react-dom";
// 定义子组件
class LifeCycleChild extends React.Component {
// ---------------初始化---------------------
constructor(props) {
console.log("进入constructor");
super(props);
// state 可以在 constructor 里初始化
this.state = { text: "子组件的文本" };
}
// 初始化/更新时调用
static getDerivedStateFromProps(props, state) {
console.log("getDerivedStateFromProps执行");
return {
fatherText: props.text
}
}
// 初始化渲染时调用
componentDidMount() {
console.log("componentDidMount执行");
}
// -----------------更新----------------------
// 组件更新时调用
shouldComponentUpdate(prevProps, nextState) {
console.log("shouldComponentUpdate执行");
return true;
}
// 组件更新时调用
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log("getSnapshotBeforeUpdate执行");
return "haha";
}
// 组件更新后调用
componentDidUpdate(preProps, preState, valueFromSnapshot) {
console.log("componentDidUpdate执行");
console.log("从 getSnapshotBeforeUpdate 获取到的值是", valueFromSnapshot);
}
// ------------------卸载---------------------
// 组件卸载时调用
componentWillUnmount() {
console.log("子组件的componentWillUnmount执行");
}
// 点击按钮,修改子组件文本内容的方法
changeText = () => {
this.setState({
text: "修改后的子组件文本"
});
};
render() {
console.log("render执行");
return (
<div className="container">
<button onClick={this.changeText} className="changeText">
修改子组件文本内容
</button>
<p className="textContent">{this.state.text}</p>
<p className="fatherContent">{this.props.text}</p>
</div>
);
}
}
// 定义 LifeCycle 组件的父组件
class LifeCycleContainer extends React.Component {
// state 也可以像这样用属性声明的形式初始化
state = {
text: "父组件的文本",
hideChild: false
};
// 点击按钮,修改父组件文本的方法
changeText = () => {
this.setState({
text: "修改后的父组件文本"
});
};
// 点击按钮,隐藏(卸载)LifeCycle 组件的方法
hideChild = () => {
this.setState({
hideChild: true
});
};
render() {
return (
<div className="fatherContainer">
<button onClick={this.changeText} className="changeText">
修改父组件文本内容
</button>
<button onClick={this.hideChild} className="hideChild">
隐藏子组件
</button>
{this.state.hideChild ? null : <LifeCycleChild text={this.state.text} />}
</div>
);
}
}
ReactDOM.render(<LifeCycleContainer />, document.getElementById("root"));