React生命周期

基础生命周期钩子

  • constructor

如果你不初始化状态,也不绑定方法,那么你就不需要为React组件实现构造函数。在这里初始化状态可以直接对this.state赋值,在这里使用props时,应当通过this.props使用;如果组件内定义的方法不是采用箭头函数默认绑定到组件实例上时,调用时应该用bind手动绑定。

  • componentWillMount(将要废弃)

在react17版本前有效的生命周期钩子,不能与新版钩子混用(componentWillRecevieProps、componentWillUpdate也是如此);
有一种常见的误区是在componentWillMount中请求数据可以避免第一次empty render,但实际中render在componentWillMount后立即执行,如果componentWillMount拉取的数据不能立即得到,那么二次渲染依然无法避免,这种情况下推荐在componentDidMount中请求数据。
所以总的来说没啥用处。

  • render

render()方法是类组件唯一必须的方法,在该钩子内,将React JSX 渲染为DOM节点。
render()函数应该是纯的,意味着不应该改变组件的状态,其每次调用都应返回相同的结果,同时它不会直接和浏览器交互

  • componentDidMount

在该钩子内,render生成的DOM节点挂载到DOM树中,在这里你可以通过ref取到DOM节点,有些需求下,可能需要DOM节点挂载后才能取到相应的属性,例如DOM节点的尺寸等,你可以立即调用setState()。它将会触发一次额外的渲染,但是它将在浏览器刷新屏幕之前发生。这保证了在此情况下即使render()将会调用两次,用户也不会看到中间状态。

  • componentWillUnmount

componentWillUnmount()紧挨着在组件被卸载和销毁之前调用。可以在该方法里处理任何必要的清理工作,例如解绑定时器,取消网络请求,清理任何在componentDidMount环节创建的订阅。

clipboard.png

稍微进阶的生命周期钩子

上述介绍的基本生命周期钩子是上图的左半部分,走完了一次初始渲染的流程,但React组件嵌套复用,当父组件更新或者组件自身状态更新触发子组件重新渲染时,合理的使用上图右半部分的生命周期钩子可以有效提升性能。

  • componentWillReceiveProps(nextProps)(将要废弃)

componentWillReceiveProps()在装载了的组件接收到新属性前调用。可能当接收到新属性后,通过if比较新旧Props不同,想要通过this.setState()来更新组件的状态。

  • shouldComponentUpdate(nextProps, nextState)

当接收到新属性或状态时,shouldComponentUpdate() 在渲染前被调用。默认为true。该方法在初始化渲染或当使用forceUpdate()时并不会被调用。
适用场景是:你想在某些状态或属性变化时,通过this.props和nextProps以及this.state 和 nextState比较,来判断是否需要重新渲染该组件,return fasle 不渲染,默认为true。不能setState。
有内建的React.PureComponent代替手写shouldComponentUpdate()。PureComponent 对属性和状态执行浅比较,因而降低你略过必要更新的机会。
但如果你需要更精细的控制,那就自定义shouldComponentUpdate吧。
但是当你的state或props是嵌套很深的引用类型时,容易由于浅比较而出错,而React文档说我们不推荐做深相等检测,或使用JSON.stringify()在shouldComponentUpdate()中,这是非常无效率的会伤害性能。所以你最好设计好自己的数据结构,不要让数据嵌套太深。

  • componentWillUpdate(nextProps, nextState)

类似于componentWillMount,在nextProps, nextState生效前最后的一次准备机会,可以读取DOM属性,为componentDidUpdate作准备(使用场景少)。不能setState。

  • componentDidUpdate

组件更新后触发,类似于componentDidMount的作用,可以在这里操作DOM,发起请求等。

clipboard.png

参考链接:
React Lifecycle Methods- how and when to use them
React组件生命周期小结

React v16.3新出的生命周期钩子

clipboard.png

  • static getDerivedStateFromProps(nextProps, prevState)

与旧的生命周期图比较可以看出,getDerivedStateFromProps处于原来的componentWillMountcomponentWillReceiveProps位置,而且this.setState()以及this.forceUpdate()也会触发该钩子。
该钩子的主要作用是为了替代componentWillReceiveProps作用。

// Before
class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
  };
  componentWillReceiveProps(nextProps) {
    if (this.props.currentRow !== nextProps.currentRow) {
      this.setState({
        isScrollingDown:
          nextProps.currentRow > this.props.currentRow,
      });
    }
  }
}
// After
class ExampleComponent extends React.Component {
  state = {
    isScrollingDown: false,
    lastRow: null,
  };

  static getDerivedStateFromProps(nextProps, prevState) {
    if (nextProps.currentRow !== prevState.lastRow) {
      return {
        isScrollingDown:
          nextProps.currentRow > prevState.lastRow,
        lastRow: nextProps.currentRow,
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}

与原有钩子相比,getDerivedStateFromProps是一个静态方法,无法访问组件实例,如此限制了其使用范围,防止不熟练的开发者自由发挥导致各种bug。该钩子主要作用可以概括为接收props来改变state。

可以发现在使用getDerivedStateFromProps时,我们在state中多花了一个变量lastRow来保存prevProps,为什么设计钩子的时候不在参数值加入prevProps呢? 官方文档的解释是:1.首次渲染也会触发getDerivedStateFromProps,这意味着每次都需要对prevProps进行空置检测,麻烦;2.不将prevProps传入是为了节省内存。

  • getSnapshotBeforeUpdate(prevProps, prevState)

取代componenWillUpdate功能。
getSnapshotBeforeUpdate在最新的渲染输出提交给DOM前将会立即调用。它让你的组件能在当前的值可能要改变前获得它们。这一生命周期返回的任何值将会 作为参数被传递给componentDidUpdate()。

class ScrollingList extends React.Component {
  listRef = null;

  getSnapshotBeforeUpdate(prevProps, prevState) {
    // Are we adding new items to the list?
    // Capture the current height of the list so we can adjust scroll later.
    if (prevProps.list.length < this.props.list.length) {
      return this.listRef.scrollHeight;
    }
    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) {
      this.listRef.scrollTop +=
        this.listRef.scrollHeight - snapshot;
    }
  }

  render() {
    return (
      <div ref={this.setListRef}>
        {/* ...contents... */}
      </div>
    );
  }

  setListRef = ref => {
    this.listRef = ref;
  };
}
  • static getDerivedStateFromError()componentDidCatch()

错误处理。。。
render phase 里产生异常的时候, 会调用 getDerivedStateFromError;

在 commit phase 里产生异常大的时候, 会调用 componentDidCatch。

componentDidCatch 是不会在服务器端渲染的时候被调用的 而getDerivedStateFromError 会

为何用推出新的钩子,废弃旧的钩子?

主要是为了适应将要推出的异步渲染,其次是避免之前生命周期钩子的滥用。

同步渲染与异步渲染

React Fiber 是在 v16 的时候引入的一个全新架构, 旨在解决异步渲染问题,但v16并没有开启。
同步渲染的痛点:当应用的组件树特别庞大时,由于javaScript是单线程的,重新渲染一旦开始,中间不会停,如果这时候用户去操作, 比如输入, 点击按钮, 此时页面是没有响应的。 等更新完了, 你之前的那些输入就会啪啪啪一下子出来了。函数调用栈如图所示:

clipboard.png

因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。

Fiber 的做法是:分片。
把一个很耗时的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。 而维护每一个分片的数据结构, 就是Fiber。
用一张图来展示Fiber 的碎片化更新过程

clipboard.png

在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。

因为一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段: render phase and commit phase.(可以从上文的生命周期图中看到)

因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。

比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。

在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用!。

总而言之就是:
render phase 可以被打断, 大家不要在此阶段做一些有副作用的操作,可以放心在commit phase 里做。

然后就是生命周期的调整, react 把你有可能在render phase 里做的有副作用的函数都改成了static 函数, 强迫开发者做一些纯函数的操作。

全面了解 React 新功能: Suspense 和 Hooks


karl
78 声望5 粉丝