从新学习 React (一) 生命周期,Fiber 调度和更新机制

前几天面试问道 react 的相关知识,对我打击比较大,感受对 react 认识很是肤浅,因此在这里从新梳理一下,想一想以前没有仔细思考过的东西。javascript

另外有说的不对的地方还请帮我指正一下,先谢谢各位啦。java

目录索引:react

什么是生命周期和调度?

React 有一套合理的运行机制去控制程序在指定的时刻该作什么事,当一个生命周期钩子被触发后,紧接着会有下一个钩子,直到整个生命周期结束。面试

生命周期

生命周期表明着每一个执行阶段,好比组件初始化,更新完成,立刻要卸载等等,React 会在指定的时机执行相关的生命周期钩子,使咱们能够有机在程序运行中会插入本身的逻辑。算法

调度

咱们写代码的时候每每会有不少组件以及他们的子组件,各自调用不一样的生命周期,这时就要解决谁先谁后的问题,在 react v16 以前是采用了递归调用的方式一个一个执行,而在如今 v16 的版本中则采用了与之彻底不一样的处理(调度)方式,名叫 Fiber,这个东西 facebook 作了有两年时间,实现很是复杂。redux

具体 Fiber 它是一个什么东西呢?不要着急,咱们先从最基本的生命周期钩子看起。性能优化

React 生命周期详解

首先看一下 React V16.4 后的生命周期概况(图片来源异步

  • 从横向看,react 分为三个阶段:
    • 建立时
      • constructor() - 类构造器初始化
      • static getDerivedStateFromProps() - 组件初始化时主动触发
      • render() - 递归生成虚拟 DOM
      • componentDidMount() - 完成首次 DOM 渲染
    • 更新时
      • static getDerivedStateFromProps() - 每次 render() 以前执行
      • shouldComponentUpdate() - 校验是否须要执行更新操做
      • render() - 递归生成虚拟 DOM
      • getSnapshotBeforeUpdate() - 在渲染真实 DOM 以前
      • componentDidUpdate() - 完成 DOM 渲染
    • 卸载时
      • componentWillUnmount() - 组件销毁以前被直接调用

一些干货

  • 有三种方式能够触发 React 更新,props 发生改变,调用 setState() 和调用 forceUpdate()
  • static getDerivedStateFromProps() 这个钩子会在每一个更新操做以前(即便props没有改变)执行一次,使用时应该保持谨慎。
  • componentDidMount()componentDidUpdate() 执行的时机是差很少的,都在 render 以后,只不过前者只在首次渲染后执行,后者首次渲染不会执行
  • getSnapshotBeforeUpdate() 执行时能够得到只读的新 DOM 树,此函数的返回值为 componentDidUpdate(prevProps, prevState, snapshot) 的第三个参数

尝试理解 Fiber

关于 Fiber,强烈建议听一下知乎上程墨Morgan的 live 《深刻理解React v16 新功能》,这里潜水员的例子和图片也是引用于此 live。async

背景

咱们知道 React 是经过递归的方式来渲染组件的,在 V16 版本以前的版本里,当一个状态发生变动时,react 会从当前组件开始,依次递归调用全部的子组件生命周期钩子,并且这个过程是同步执行的且没法中断的,一旦有很深很深的组件嵌套,就会形成严重的页面卡顿,影响用户体验。函数

React 在V16版本以前的版本里引入了 Fiber 这样一个东西,它的英文涵义为纤维,在计算机领域它排在在进程和线程的后面,虽然 React 的 Fiber 和计算机调度里的概念不同,可是能够方便对比理解,咱们大概能够想象到 Fiber 多是一个比线程还短的时间片断。

Fiber 到底作了什么事

Fiber 把当前须要执行的任务分红一个个微任务,安排优先级,而后依次处理,每过一段时间(很是短,毫秒级)就会暂停当前的任务,查看有没有优先级较高的任务,而后暂停(也可能会彻底放弃)掉以前的执行结果,跳出到下一个微任务。同时 Fiber 还作了一些优化,能够保持住以前运行的结果以到达复用目的。

举个潜水员的例子

咱们能够把调度当成一个潜水员在海底寻宝,v16 以前是经过组件递归的方式进行寻宝,从父组件开始一层一层深刻到最里面的子组件,也就是以下图所示。

而替换成了 Fiber 后,海底变成的狭缝(简单理解为递归变成了遍历),潜水员会每隔一小段时间浮出水面,看看有没有其余寻宝任务。注意此时没有寻到宝藏的话,那么以前潜水的时间就浪费了。就这样潜水员会一直下潜和冒泡,具体以下图所示。

引入 Fiber 后带来的三个阶段

从生命周期那张图片纵向来看,Fiber 将整个生命周期分红了三个阶段:

  • render 阶段
    • 因为 Fiber 会时不时跳出任务,而后从新执行,会致使该阶段的生命周期调用屡次的现象,因此 React V16 以前 componentWillMount()componentWillUpdate()componentWillReceiveProps() 的三个生命周期钩子被加上了 UNSAFE 标记
    • 这个阶段效率不必定会比以前同步递归来的快,由于会有任务跳出重作的性能损耗,可是从宏观上看,它不断执行了最高优先级(影响用户使用体验)的任务,因此用户使用起来会比之前更加的流畅
    • 这个阶段的生命周期钩子可能会重复调用,建议只写无反作用的代码
  • pre-commit 阶段
    • 该阶段 DOM 已经造成,但仍是只读状态
    • 这个阶段组件状态不会再改变
  • commit 阶段
    • 此时的 DOM 能够进行操做
    • 这个阶段组件已经完成更新,能够写一些有反作用的代码和添加其它更新操做。

简而言之:以 render() 为界,以前执行的生命周期都有可能会打断并屡次调用,以后的生命周期是不可被打断的且只会调用一次。因此尽可能把反作用的代码放在只会执行一次的 commit 阶段。

其它生命周期钩子

除了上面经常使用的钩子,React 还提供了以下钩子:

  • static getDerivedStateFromError() 在 render 阶段执行,经过返回 state 更新组件状态
  • componentDidCatch() 在 commit 阶段执行,能够放一些有反作用的代码

更新机制

理解了生命周期和三个执行阶段,就能够比较容易理解组件状态的更新机制了。

setState()

这个方法可让咱们更新组件的 state 状态。第一个参数能够是对象,也能够是 updater 函数,若是是函数,则会接受当前的 state 和 props 做为参数。第二个参数为函数,是在 commit 阶段后执行,准确的说是在 componentDidUpdate() 后执行。

setState() 的更新过程是异步的(除非绑定在 DOM 事件中或写在 setTimeout 里),并且会在最后合并全部的更新,以下:

Object.assign(
  previousState,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)
复制代码

之因此设计成这样,是为了不在一次生命周期中出现屡次的重渲染,影响页面性能。

forceUpdate()

若是咱们想强制刷新一个组件,能够直接调用该方法,调用时会直接执行 render() 这个函数而跳过 shouldComponentUpdate()

举个极端例子

function wait() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve();
      console.log("wait");
    }, 0);
  });
}

//......省略组件建立
async componentDidMount() {
  await wait();
  this.setState({
    name: "new name"
  });
  console.log("componentDidMount");
}

componentDidUpdate() {
  console.log("componentDidUpdate");
}

render() {
  console.log(this.state);
  return null
}
//......省略组件建立

// 输出结果以下
// wait
// {name: "new name"}
// componentDidUpdate
// componentDidMount

// 注意 componentDidUpdate 的输出位置,通常状况下
// componentDidUpdate 都是在componentDidMount 后面
// 执行的,可是这里由于setState 写在了 await 后面
// 因此状况相反。
复制代码

结语

了解 react 生命周期和更新机制确实有利于编写代码,特别是当代码量愈来愈大时,错用的 setState 或生命周期钩子均可能埋下愈来愈多的雷,直到有一天没法维护。。。

个人我的建议以下:

  • 把反作用代码统统放在 commit 阶段,由于这个阶段不会影响页面渲染性能
  • 尽量不要使用 forceUpdate() 方法,借用 Evan You 的一句话,若是你发现你本身须要在 Vue 中作一次强制更新,99.9% 的状况,是你在某个地方作错了事
  • 只要调用了 setState() 就会进行 render(),不管 state 是否改变
  • 知道 setState() 更新的何时是同步的,何时是异步的,参见上文
  • 不要把 getDerivedStateFromProps() 当成是 UNSAFE_componentWillReceiveProps() 的替代品,由于 getDerivedStateFromProps() 会在每次 render() 以前执行,即便 props 没有改变
相关文章
相关标签/搜索