大白话 在React中的生命周期方法有哪些,在一个组件加载过程中这些生命周期都干了些什么,在项目中你常用到的生命周期方法有哪些?
引言
咱前端工程师谁还没在面试时被React生命周期问懵过啊?明明项目里天天用组件,可一被追问“组件从生到死都经历了哪些步骤”“某个方法在啥时候执行”,就跟玩游戏不知道关卡流程一样,脑子瞬间卡壳。就像你煮面条,知道要加水、下面、加盐,但问你水开的温度、煮面的时长这些细节,可能就含糊了。
其实啊,React生命周期没那么玄乎,它就像组件的“一生”,从出生、成长到死亡,每个阶段都有对应的“任务”。掌握了它,写组件时能少踩很多坑,性能优化也能找对方向。这篇文章就像“布洛芬”,帮你缓解面对生命周期时的头大,用大白话把每个关卡讲明白,让你看完就能理清思路,面试不慌,项目顺手。
技术原理
什么是React生命周期
React生命周期就是组件从创建、渲染到更新、卸载的整个过程中,会自动触发的一系列方法的集合。你可以把它想象成一个人从出生、长大、变老到死亡的一生,每个阶段都有特定的事情要做,组件也一样,每个生命周期方法都有它该干的活儿。
生命周期的三个阶段及对应方法
挂载阶段(组件出生)
这个阶段组件从无到有,首次被渲染到DOM中,就像一个宝宝刚出生,要办理出生证、登记信息等。
- constructor:组件的构造函数,在组件被创建时最先调用。就像给新生儿办出生证,主要用来初始化组件的state,以及给事件处理函数绑定this。不过要注意,如果你在里面用了super(props),才能在构造函数里访问this.props。
- static getDerivedStateFromProps:这个方法比较特殊,它是静态方法,在组件挂载和更新时都会被调用。作用是根据props来更新state,返回一个对象来更新state,如果返回null就不更新。它就像一个“翻译官”,把props里的信息转换成组件内部的state。
- render:这是一个必须有的方法,负责渲染组件的UI。它就像画家画画,根据state和props画出组件的样子。需要注意的是,render方法不能包含副作用操作,比如修改state、调用API等,它应该是纯函数,只根据输入返回对应的输出。
- componentDidMount:组件挂载完成后调用,也就是组件已经被渲染到DOM中了。这时候可以进行一些副作用操作,比如调用API获取数据、设置定时器、绑定事件监听等。就像宝宝出生后,医生检查没问题,家长可以开始给宝宝喂奶、换尿布了。
更新阶段(组件成长)
当组件的props或者state发生变化时,就会进入更新阶段,组件会重新渲染,就像人在成长过程中,身高、体重等发生变化一样。
- static getDerivedStateFromProps:和挂载阶段一样,在更新时也会被调用,还是根据props更新state。
- shouldComponentUpdate:决定组件是否需要重新渲染,返回布尔值,true表示需要重新渲染,false表示不需要。它就像一个“门卫”,判断是不是有必要让组件更新,能起到优化性能的作用,避免不必要的渲染。比如当props或state的变化不会影响UI时,就可以返回false。
- render:更新阶段也会调用render方法,重新渲染UI。
- getSnapshotBeforeUpdate:在组件更新前获取DOM的快照,返回的值会作为参数传给componentDidUpdate。比如可以在这里获取滚动位置,在更新后恢复滚动位置。就像搬家前,先拍张照片记录家具的位置,搬完后根据照片摆放。
- componentDidUpdate:组件更新完成后调用。可以在这里处理更新后的操作,比如根据新的props调用API,或者操作DOM。不过要注意,这里如果要修改state,一定要加判断条件,否则会导致无限循环更新。
卸载阶段(组件死亡)
当组件从DOM中被移除时,就进入了卸载阶段,就像人去世后要处理后事一样。
- componentWillUnmount:在组件卸载和销毁前调用。主要用来清理一些副作用,比如清除定时器、取消事件监听、取消API请求等,防止内存泄漏。就像离开房间前要关灯、关空调一样,做好收尾工作。
代码示例
下面用一个简单的计数器组件来展示React生命周期的各个方法:
import React, { Component } from 'react';
class Counter extends Component {
// 1. 构造函数,组件创建时最先调用
constructor(props) {
super(props); // 调用父类的构造函数,传入props
// 初始化state,设置计数器初始值为0
this.state = {
count: 0,
timer: null // 用来存储定时器ID
};
console.log('constructor:组件构造中,初始化state');
}
// 2. 静态方法,根据props更新state
static getDerivedStateFromProps(nextProps, prevState) {
console.log('getDerivedStateFromProps:根据props更新state');
// 如果父组件传入了初始值,就用父组件的初始值更新state
if (nextProps.initialCount !== prevState.count && prevState.count === 0) {
return { count: nextProps.initialCount };
}
return null; // 不更新state
}
// 3. 处理点击增加计数的方法
handleIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
// 4. 处理点击减少计数的方法
handleDecrement = () => {
this.setState({ count: this.state.count - 1 });
};
// 5. 决定组件是否需要更新
shouldComponentUpdate(nextProps, nextState) {
console.log('shouldComponentUpdate:判断是否需要更新');
// 当计数是偶数时才更新,奇数时不更新,这里只是举例,实际根据需求判断
return nextState.count % 2 === 0;
}
// 6. 获取更新前的DOM快照
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('getSnapshotBeforeUpdate:获取更新前的快照');
// 这里可以获取一些DOM信息,比如滚动位置等,这里只是举例返回当前计数
return { prevCount: prevState.count };
}
// 7. 组件挂载完成后调用
componentDidMount() {
console.log('componentDidMount:组件挂载完成');
// 设置一个定时器,每5秒自动增加计数
const timer = setInterval(() => {
this.setState({ count: this.state.count + 1 });
}, 5000);
this.setState({ timer });
}
// 8. 组件更新完成后调用
componentDidUpdate(prevProps, prevState, snapshot) {
console.log('componentDidUpdate:组件更新完成');
console.log('更新前的计数是:', snapshot.prevCount);
// 如果计数达到10,就停止定时器
if (this.state.count >= 10) {
clearInterval(this.state.timer);
}
}
// 9. 组件卸载前调用
componentWillUnmount() {
console.log('componentWillUnmount:组件即将卸载');
// 清除定时器,防止内存泄漏
clearInterval(this.state.timer);
}
// 10. 渲染组件UI
render() {
console.log('render:渲染组件');
return (
<div>
<h2>计数器:{this.state.count}</h2>
<button onClick={this.handleIncrement}>增加</button>
<button onClick={this.handleDecrement}>减少</button>
</div>
);
}
}
export default Counter;
对比效果
生命周期方法 | 触发时机 | 作用 | 注意事项 |
---|---|---|---|
constructor | 组件创建时,挂载阶段最先调用 | 初始化state,绑定事件处理函数 | 避免在这里调用setState,会导致额外渲染;如果不初始化state和绑定方法,可以不写 |
static getDerivedStateFromProps | 挂载和更新阶段,在render之前调用 | 根据props更新state | 是静态方法,不能访问this;返回对象更新state,返回null不更新;容易导致代码复杂,谨慎使用 |
render | 挂载和更新阶段都会调用 | 渲染组件UI | 是纯函数,不能包含副作用;必须返回JSX、null等 |
componentDidMount | 挂载阶段,组件被渲染到DOM后调用 | 执行副作用操作,如调用API、设置定时器等 | 可以在这里调用setState,会触发重新渲染,但要注意性能 |
shouldComponentUpdate | 更新阶段,在render之前调用 | 判断组件是否需要重新渲染 | 返回布尔值,true表示需要,false表示不需要;主要用于性能优化 |
getSnapshotBeforeUpdate | 更新阶段,在render之后,DOM更新之前调用 | 获取DOM更新前的快照 | 返回的值会传给componentDidUpdate;可以获取滚动位置等DOM信息 |
componentDidUpdate | 更新阶段,DOM更新后调用 | 处理更新后的操作,如根据新数据调用API等 | 可以在这里调用setState,但必须加判断条件,否则会无限循环;prevProps和prevState用于比较前后变化 |
componentWillUnmount | 卸载阶段,组件被移除前调用 | 清理副作用,如清除定时器、取消事件监听等 | 不能在这里调用setState,因为组件即将卸载,不会重新渲染 |
面试题的正常回答和大白话回答
正常回答
React的生命周期主要分为挂载、更新和卸载三个阶段。
挂载阶段:constructor用于初始化state和绑定方法;static getDerivedStateFromProps根据props更新state;render负责渲染UI;componentDidMount在组件挂载后执行副作用操作。
更新阶段:当props或state变化时触发,static getDerivedStateFromProps再次根据props更新state;shouldComponentUpdate判断是否需要更新;render重新渲染;getSnapshotBeforeUpdate获取DOM快照;componentDidUpdate处理更新后的操作。
卸载阶段:componentWillUnmount用于清理副作用。
大白话回答
React组件就像人一样有一生。出生(挂载)的时候,先办“出生证”(constructor)初始化信息,然后根据外部信息调整自己(getDerivedStateFromProps),接着“长”出样子(render),最后彻底“落地”了(componentDidMount)就可以干些正事,比如发请求。
长大(更新)的时候,先看看外部信息有没有变,要不要调整自己(getDerivedStateFromProps),再决定要不要“变样子”(shouldComponentUpdate),然后重新“长”样子(render),更新前先拍个“快照”(getSnapshotBeforeUpdate),更新完了再处理后续事(componentDidUpdate)。
死亡(卸载)的时候,就要把自己留下的“烂摊子”收拾好(componentWillUnmount),比如关掉定时器。
总结
React生命周期是组件从创建到销毁过程中一系列自动触发的方法,分为挂载、更新和卸载三个阶段。每个阶段的方法都有其特定的作用,constructor初始化基础信息,render负责展示外观,componentDidMount和componentDidUpdate处理各种外部交互,shouldComponentUpdate优化性能,componentWillUnmount做好收尾工作。
了解这些生命周期方法,能帮助我们更好地控制组件的行为,避免不必要的错误和性能问题。就像知道了游戏的关卡流程,才能更顺利地通关一样,掌握生命周期,写React组件也能更得心应手。
扩展思考及解答
1. React Hooks出现后,生命周期方法还有必要学吗?
有必要。虽然Hooks(如useState、useEffect)可以替代大部分生命周期方法,让代码更简洁,但Hooks的设计理念是建立在对生命周期的理解之上的。比如useEffect可以模拟componentDidMount、componentDidUpdate和componentWillUnmount的组合效果。
了解生命周期方法,能帮助我们更好地理解Hooks的工作原理,知道什么时候该用useEffect,以及如何正确设置依赖项。而且在维护一些旧项目时,可能还会遇到使用类组件和生命周期方法的代码,所以学习生命周期方法仍然很有必要。
2. 如何优化生命周期中的性能问题?
- 合理使用shouldComponentUpdate:通过比较前后props和state的变化,决定是否需要重新渲染,避免不必要的渲染。
- 避免在render方法中创建新函数或对象:因为每次render都会创建新的引用,可能导致子组件的shouldComponentUpdate判断失效,引发不必要的更新。
- 使用React.memo:对函数组件进行包装,相当于给组件加了一个shouldComponentUpdate,浅比较props,相同则不重新渲染。
- 减少componentDidUpdate中的不必要操作:在里面调用setState一定要加判断条件,避免无限循环;只在必要时执行耗时操作。
- 合理使用getDerivedStateFromProps:不要过度使用,否则会让组件逻辑复杂,难以维护,它主要用于props决定state的场景。
3. 父子组件生命周期的执行顺序是怎样的?
挂载阶段:父组件的constructor → 父组件的getDerivedStateFromProps → 父组件的render → 子组件的constructor → 子组件的getDerivedStateFromProps → 子组件的render → 子组件的componentDidMount → 父组件的componentDidMount。
更新阶段:父组件的getDerivedStateFromProps → 父组件的shouldComponentUpdate → 父组件的render → 子组件的getDerivedStateFromProps → 子组件的shouldComponentUpdate → 子组件的render → 子组件的getSnapshotBeforeUpdate → 父组件的getSnapshotBeforeUpdate → 子组件的componentDidUpdate → 父组件的componentDidUpdate。
卸载阶段:父组件的componentWillUnmount → 子组件的componentWillUnmount。
简单来说,就是父组件先开始,但子组件先完成挂载和更新,卸载时父组件先准备卸载,子组件先完成卸载。
4. 错误边界与生命周期有什么关系?
错误边界是React中用于捕获子组件树中的JavaScript错误,并渲染备用UI的组件。它利用了两个生命周期方法:static getDerivedStateFromError和componentDidCatch。
static getDerivedStateFromError:当子组件抛出错误时调用,返回一个对象来更新state,用于渲染备用UI。
componentDidCatch:同样在子组件抛出错误时调用,用于记录错误信息,比如发送到日志服务。
错误边界不能捕获自身的错误,也不能捕获异步代码(如setTimeout)和事件处理函数中的错误,它主要针对渲染过程、生命周期方法和子组件的构造函数中的错误。通过这两个生命周期方法,错误边界实现了对错误的处理,防止整个应用崩溃。
结尾
看完这篇文章,是不是觉得React生命周期没那么难了?就像玩游戏摸清了关卡流程,心里踏实多了。这些生命周期方法就像组件的“说明书”,了解它们,你就能更好地驾驭组件,写出更稳定、性能更好的代码。
在实际项目中,根据需求灵活运用这些方法,比如在componentDidMount里发请求,在componentWillUnmount里清定时器,用shouldComponentUpdate优化性能。遇到问题时,想想组件处于哪个“人生阶段”,该用哪个方法解决,就像生病时知道该吃哪种药一样。
如果你在使用React生命周期时遇到过什么奇葩问题,或者有自己的小技巧,欢迎在评论区留言分享,咱们一起交流学习,让前端之路走得更顺!记住,掌握生命周期,就像给组件装上了“导航”,再也不怕迷路啦!