含泪总结N久的React生命周期--前端面试必问

281 阅读7分钟

React生命周期

在之前的React版本中,如果有一个很复杂的复合组件,然后改动了最上层组件的 state,那么调用栈可能会很长,再加上中间进行了复杂的操作,可能导致长时间阻塞主线程,带来不好的用户体验。React Fiber应运而生。

Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将之前的同步渲染改成了异步渲染,在不影响体验的情况下去分段计算更新。

React 如何区别优先级: 对于动画这种实时性很高的东西,也就是 16 ms 必须渲染一次保证不卡顿的情况下,React 会每 16 ms(以内) 暂停一下更新,返回来继续渲染动画。

异步渲染有两个阶段:reconciliation(调和阶段) commit(提交),前者过程是可以打断的,后者不能暂停,会一直更新界面直到完成。

组件的生命周期可分成三个状态:

  • Mounting:已插入真实 DOM
  • Updating:正在被重新渲染
  • Unmounting:已移出真实 DOM

生命周期方法

Constructor()

React借用class类的constructor充当初始化钩子, 因为我们只允许通过特定的途径给组件传递参数,所以constructor的参数实际上是被React规定好的。React规定constructor有三个参数,分别是props、context和updater。

  • props是属性,它是不可变的。
  • context是全局上下文。
  • updater是包含一些更新方法的对象

this.setState最终调用的是this.updater.enqueueSetState方法,this.forceUpdate最终调用的是this.updater.enqueueForceUpdate方法,所以这些API更多是React内部使用,暴露出来是以备开发者不时之需。

在React中,因为所有class组件都要继承自Component类或者PureComponent类,因此和原生class写法一样,要在constructor里首先调用super方法,才能获得this。constructor生命周期钩子的最佳实践是在这里初始化this.state

当然,你也可以使用属性初始化器来代替,如下:

import React, { Component } from 'react';
class App extends Component {
    state = {name: 'biu',};
}
export default App;

setState会怎样?(我说怎么会在这里setState,面试官说如果就是有人这么手欠呢?hhh)

setState是无效的,setState(...): 仅更新mounted 或者 mounting的组件,在constructor中,组件还没有mounted/mounting ,所以会报错,被忽略掉这里的setState。

Reconciliation阶段(可打断)

componentWillMount()

在组件挂载到DOM之前(渲染前)调用, 在客户端也在服务端(server-side-rendering服务端渲染SSR)

不在这里请求数据:componentWillMount和挂载是同步执行的,意味着执行完这个钩子,立即挂载。而向服务器请求数据是异步执行的。所以无论请求怎么快,都要排在同步任务之后再处理,也就是说,永远不可能在这里将请求的数据插入元素一同挂载。(在didMount里面调用异步ajax请求)

componentWillReceiveProps(newProps)

在组件接收到新的 prop时被调用, 初始化不触发。

  • 组件接收到了新的属性
  • 组件没有收到新的属性,但是由于父组件重新渲染导致当前组件也被重新渲染。

shouldComponentUpdate (nextProps, nextState)

返回一个布尔值,判断是否更新,用来优化性能(默认返回true)。在组件接收到新的props或者state时被调用。在初始化时或者使用forceUpdate时不被调用。可以在你确认不需要更新组件时使用。(PureComponent自动实现了浅比较的shouldComponentUpdate())

注:可以用在比较少进行修改的组件上,因为如果如果一个组件比较常修改,就不用浪费时间去自己判断了,比如说 父组件里setstate之后 会调用父组件的render 子组件也都要更新 但是有些子组件可能没有用到父组件的状态 或者是一部分很少改变的状态,就调用shouldComponentUpdate设置让他不要更新

componentWillUpdate(nextProps, nextState)

在组件接收到新的props或者state(shouldComponentUpdate返回true/调用this.forceUpdate()的时候)但还没有render时被调用,在初始化时不会被调用。(慎用setState(),可能无限循环)

注:除了shouldComponentUpdate,其他三个正在逐渐废弃,弄了个别名 UNSAFE_componentWillXXX

render()

构建抽象UI(要挂载到DOM上的元素),千万不要在render生命周期钩子里调用this.setState,因为this.setState会引发render,陷入无限循环。

Commit阶段(一直更新到完成)

componentDidMount()

在第一次渲染后调用,只在客户端。

使用render方法返回的虚拟DOM对象来创建真实的DOM结构, 之后组件已经生成了对应的DOM结构,可以通过this.getDOMNode()来进行访问。 如果你想和其他JavaScript框架一起使用,可以在这个方法中调用setTimeout, setInterval或者发送AJAX请求数据等操作。

为什么在这里请求数据:

请求得到的数据一般是要加载到对应的dom的,异步请求的结果在第一次渲染前还没有拿到,如果没有设置state的这个拿到的数据属性的初始值(constructor中this.state设置初始值/defaultProps设置这个子组件的初始props),会报map undefined的错误, 如果设置了初始值,也会用空的数据进行至少一次render。

在didmount这里进行请求数据,可以更加明确上述过程是:

先渲染了再进行的请求获取数据,根据拿到的数据setState再渲染一次加载数据到dom上。

不在willmount中请求数据是因为:

willmount正在被废弃,且由于fiber可能重复执行,服务端&客户端都会执行一次,didmount只在客户端执行。

componentDidUpdate (nextProps, nextState, snapshot)

在组件完成更新后立即调用。在初始化时不会被调用(慎用setState())

componentWillUnmount

在组件从 DOM 中移除之前立刻被调用。组件中用到的原生的事件监听器(react的合成事件会自己销毁)、订阅器、定时器都要在这里销毁


因为 Reconciliation 阶段是可以被打断的,所以 Reconciliation 阶段会执行的生命周期函数就可能会出现调用多次的情况,从而引起 Bug。由此对于 Reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 以外,其他都应该避免去使用,并且 V16 中也引入了新的 API 来解决这个问题。

解决方法(引入的新方法)

getDerivedStateFromProps(nextProps, prevState)

用于替换 componentWillReceiveProps ,该函数会在初始化和 update 时被调用,根据父组件传来的props按需更新自己的state(衍生state)。返回的对象就是要增量更新的state。

它被设计成静态(static)方法, 这样开发者就访问不到this也就是实例了,也就不能在里面调用实例方法或者setsState, 目的是保持该方法的纯粹,它就是用来定义衍生state的,除此之外不应该在里面执行任何操作。

class ExampleComponent extends React.Component {
// Initialize state in constructor,
// Or with a property initializer.
    state = {};
    static getDerivedStateFromProps(nextProps, prevState) {
        if (prevState.someMirroredValue !== nextProps.someValue) {
            return {
                derivedData: computeDerivedState(nextProps),
                someMirroredValue: nextProps.someValue};
            }
    // Return null to indicate no change to state.
        return null;
        }
    }

getSnapshotBeforeUpdate(prevProps, prevState)

    用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。返回的值会被componentDidUpdate的第三个参数接收,我们可以利用这个通道保存一些不需要持久化的状态,用完即可舍弃,有一些状态,比如网页滚动位置,我不需要它持久化,只需要在组件更新以后能够恢复原来的位置即可。

    componentDidCatch(err,info)

      它主要用来捕获错误并进行相应处理,所以它的用法也比较特殊。

      定制一个只有componentDidCatch生命周期钩子的ErrorBoundary组件,它只做一件事:如果捕获到错误,则显示错误提示,如果没有捕获到错误,则显示子组件。将需要捕获错误的组件作为ErrorBoundary的子组件渲染,一旦子组件抛出错误,整个应用依然不会崩溃,而是被ErrorBoundary捕获。


      参考资料:

      juejin.cn/post/684490…

      daveceddia.com/where-fetch…