React生命周期中有哪些坑?如何避免?
在讨论React
的生命周期的时候,一定是在讨论类组件,因为函数组件并没有生命周期的概念,它本身就是一个函数,只会从头执行到尾巴
其实生命周期只是一个抽象的概念,大部分人看到生命周期想到的往往都componentDidMount,componentWilMount等等函数,然而这些其实并不是它的生命周期,只是在生命周期中按顺序执行的函数而已,挂载 --> 更新 --> 卸载 这一React的完整流程才叫生命周期挂载阶段 – 指的是组件从初始化到完成加载的过程
constructor
constructor
是类通用的构造函数,常用于初始化,所以在过去,constructor
常用于初始化state
和绑定函数,例如:
import React from 'react'; class Counter extends React.Component { constructor(props) { super(props) this.state = { count: 0, } this.handleClick = this.handleClick.bind(this) } handleClick() { // do some thing } render() { return null } }
那为什么会强调过去呢,因为当类属性开始流行之后,React
社区的写法发生了变化,即去除了 constructor
。
import React from 'react' class Counter extends React.Component { state = { count: 0, } // 类属性第三阶段提案 handleClick = () => { // do some thing } render() { return null } }
社区去除 constructor
的原因很明确:
constructor
中并不推荐去处理初始化以外的逻辑constructor
本身并不是属于React
的生命周期,它只是Class
的一个初始化函数
- 通过移除
constructor
, 代码会变的更加的简洁
getDerivedStateFromProps
这个函数的作用是当 props
发生变化时来更新state
,那么它的触发时机是什么时候呢?
- 当 props 被传入的时候
- 当 state 发生变化时
- 当 forceUpdate 被调用时
最常见的一个错误就是误认为只有当props发生变化时,getDerivedStateFromProps 才会被调用,而实际上只要父组件重新渲染时,getDerivedStateFromProps 就会被调用,所以是外部参数,
也就是 props 传入时就会发生变化。下面是官方文档中的列子:
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 } }
依据官方的说法,它的使用场景是很有限的。
两种反模式的使用方式:
- 直接复制到 props 到 state
- 在 props 变化后修改 state
这两种写法除了增加代码的维护成本外,没有带来任何的好处。
UNSAFE_componentWillMount
也就是componentWillMount
,在组件即将被挂载之前执行某些操作,目前已被弃用,因为在React
的新的异步渲染机制下,该方法可能会被多次调用。
写过服务端渲染的同学应该会遇到过, componentWillMount
跟服务器端同构渲染的时候,如果在该函数中发起网络请求话,会发现请求在服务器和客户端各执行了一次,所以React
更推荐在componentDidMount
中去做网络请求等操作
render
render
函数返回的是 JSX
的数据结构,用于描述具体的渲染内容,但是 render
函数并不会去真正的渲染组件,真正的渲染是依靠 React
操作 JSX
描述结构来完成,而且 render
函数应该是一个纯函数,不应该在里面产生副作用,比如调用 setState
函数(render
函数在每次渲染的时候都会被调用,而 setState
又会触发渲染,所以就会造成死循环)
componentDidMount
componentDidMount
用于在组件挂载完成时去做某些操作,比如发起网络请求等,
componentDidMount
是在render
之后被调用
至此 挂载阶段 基本算是完成
更新阶段
更新阶段是指:当外部
props
被传入,又或者当state
发生改变时的阶段。
UNSAFE_componentWillReceiveProps
该函数已标记弃用,因为其功能可被 getDerviedStateFromProps
所替代
另外,当 getDerviedStateFromProps
存在时 UNSAFE_componentWillReceiveProps
不会被调用
getDerviedStateFromProps
同挂载阶段的表现一致。
shouldComponentUpdate
该方法通过返回true
或者fasle
来确定是否重新触发新的渲染,这也是性能优化的必争之地,通过添加判断条件来控制组件是否需要重新渲染
当前 React
的官方也给出了一个通用的优化方案,那就是PureComponent,PureComponent
的核心原理就是默认实现了一个 shouldComponentUpdate
函数,在这个函数中对 props
和 state
进行浅比较,用来判断是否重新触发更新。
shouldComponentUpdate(nextProps, nextState) { // 浅比较仅比较值与引用,并不会对 Object 中的每一项值进行比较 if (shadowEqual(nextProps, this.props) || shadowEqual(nextState, this.state) ) { return true } return false }
UNSAFE_componentWillUpdate
该方法也被标记弃用,因为在后续的 React
的异步渲染设计中,可能会出现组件暂停更新渲染的情况
render
同挂载阶段的表现一致
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate
方法是配合 React
新的异步渲染的机制,在DOM
更新发生前被调用,其返回值将作为 componentDidUpate
的第三个参数
官方案例:
class ScrollingList extends React.Component { constructor(props) { super(props); this.listRef = React.createRef(); } getSnapshotBeforeUpdate(prevProps, prevState) { // Are we adding new items to the list? // Capture the scroll position so we can adjust scroll later. if (prevProps.list.length < this.props.list.length) { const list = this.listRef.current; return list.scrollHeight - list.scrollTop; } return null; } componentDidUpdate(prevProps, prevState, snapshot) { // If we have a snapshot value, we've just added new items. // Adjust scroll so these new items don't push the old ones out of view. // (snapshot here is the value returned from getSnapshotBeforeUpdate) if (snapshot !== null) { const list = this.listRef.current; list.scrollTop = list.scrollHeight - snapshot; } } render() { return ( <div ref={this.listRef}>{/* ...contents... */}</div> ); } }
componentDidUpdate
如上述案例,getSnapshotBeforeUpdate
的返回值会作为 componentDidUpdate
的第三个参数适用。
copinentDidUpdate
中可以设置 setState
,会触发渲染,慎用,避免死循环
至此 更新阶段 基本算是完成
卸载阶段
React 的卸载阶段就容易多啦,只有一个函数
componentWillUnmount
该函数的作用就是用来清理工作,如果项目中有用到定时器啥的,一定记得要在这清除掉,否则就会一直执行~
职责梳理
在梳理了生命周期后,需要注意两个问题
- 什么情况下会触发重新渲染
- 渲染过程中的报错该如何处理
带着上述两个问题,下面咱们来一一分析3种重新渲染和错误处理的情况
函数组件
函数组件在任何情况下都会重新渲染,它并没有生命周期,但是官方提供了一种优化的方式,那就是 React.memo
函数
const MyComponent = React.memo(function MyComponent(props) { /* 使用 props 渲染 */ });
React.memo
并不会阻断渲染,而是跳过渲染组件的操作并直接复用最近一次渲染的结果,这与 shouldComponentUpdate
是完全不同的
React.Component
如果不实现 shouldComponentUpdate
函数,那么下面这两种情况回引发重新渲染
- 当
state
发生变化时 - 当父组件的
Props
传入时,无论props
有没有发生变化, 只要传入就会引发重新渲染
React.PureComponent
PureComponent
默认实现了 shouldComponentUpdate
函数。所以仅在 props
与 state
进行浅比较后,确认有变更时才会触发重新渲染。
错误边界
错误边界其实就是一种
React
组件,这种组件可以捕获并打印发生在其子组件树任何位置的JavaScript
错误,并且,它会渲染备用组件。
如下官方案例
class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 同样可以将错误日志上报给服务器 logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 可以自定义降级后的 UI 并渲染 return <h1>Something went wrong.</h1>; } return this.props.children; } }
但是渲染时的报错,只能通过componentDidCatch
捕获,这是在做线上页面报错监控时,极其容易忽略的点。
综上所述,咱们就可以总结这个问题了,如下:
避免生命周期中的坑需要做好两件事:
1、不在恰当的时候调用了不该调用的代码;
2、在需要调用时,不要忘了调用。
那么下面7种情况最容易造成生命周期的坑:
getDerivedStateFromProps
容易编写反模式代码,使受控组件和非受控组件区分模糊componentWillMount
在React
中已被标记弃用,不推荐使用,主要的原因是因为新的异步架构- 会导致它被多次调用,所以网络请求以及事件绑定应该放到
componentDidMount
中 componentWillReceiveProps
同样也被标记弃用,被getDerivedStateFromProps
所取代,主要原因是性能问题。shouldComponentUpdate
通过返回true
或者false
来确定是否需要触发新的渲染。主要用于性能优化。componentWillUpdate
同样是由于新的异步渲染机制,而被标记废弃,不推荐使用,原先的逻辑- 可结合
getSnapshotBeforeUpdate
与componentDidUpdate
改造使用。 - 如果在
componentWillUnmount
函数中忘记解除事件绑定,取消定时器等清理操作,容易引发bug
。
- 如果没有添加错误边界处理,当渲染发生异常时,用户将会看到一个无法操作的白屏,所以一定要添加。
追问:React 的请求应该放到哪里?为什么?
对于异步请求,应该放到componentDidMount
中去操作,从生命周期函数执行顺序来看,除了componentDidMount
之外还有下面两种选择:
constructor
中可以放,但是不推荐,因为constructor
主要用于初始化state
和函数绑定,并不承载业务逻辑,而且随着类属性的流行,constructor
很少用
componentWillMount
已被标记废弃,因为在新的异步渲染下该方法会触发多次渲染,容易引发bug
,不利于React
的后期维护和更新
所以React
的请求放在 componentDidMount
里是最好的选择