在讨论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 } }
依据官方的说法,它的使用场景是很有限的。
两种反模式的使用方式:
1.直接复制到 props 到 state
2.在 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 函数,那么下面这两种情况回引发重新渲染
1.当 state 发生变化时
2.当父组件的 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 里是最好的选择