“如何防止坑?”换种思维思考也就是“为什么会有坑?”在代码编写中,遇到的坑往往会有两种:

  • 在不失当的机会调用了不适合的代码
  • 在须要调用的时候,没有调用

-- 来自:伯约文章

要防止生命周期的坑,就须要先理解React有那些生命周期?在React的不同版本生命周期的钩子函数也大同小异。React的生命周期分为三个挂载、更新、销毁阶段,不同的阶段触发不必的钩子函数。接下来咱们就一一来看看。

React 15生命周期

生命周期测试例子,测试版本React 15.7.0

组件的初始化渲染(挂载)

constructor

constructor是类的构造函数,在组件初始化的时候只会执行一次,用于初始化state和绑定函数。

constructor(props) {    console.log("进入constructor");    super(props);    this.state = { text: "这个子组件文本" }; }

然而随着类属性的风行,我在很多的代码中看到不在写constructor,而是改用类属性。移除constructor的起因无非就是:

  • 让代码变得更加简洁
  • constructor并不是React生命周期的一部分
class LifeCycelContainer extends React.Component {  state = {    text: "组件文本",    hideChild: false  };  render() {    return (      <div className="fatherContainer">        {this.state.text}      </div>    );  }}
componentWillMount

该办法也是也是在挂载的时候调用一次,并且办法在render办法之前调用。该办法在React前期的版本就曾经标记废除。起因是在React异步机制下,该生命周期钩子可能会被屡次调用。最直观的一个例子,在该办法中写了异步申请,那有可能会被屡次触发。

render

render办法并不会去真正的操作DOM,它的作用是把须要的货色返回回来。真正渲染的工作,是挂载阶段的ReactDOM.render办法去操作。

componentDidMount

componentDidMount办法执行,意味着初始化挂载的操作根本实现。它次要用于组件加载实现时做某些操作,比方发动网络申请、绑定事件或者你曾经能够对DOM进行操作了,该函数是接着 render 之后调用的。但 componentDidMount 肯定是在实在 DOM 绘制实现之后调用吗?在浏览器端,咱们能够这么认为。

但在其余场景下,尤其是 React Native 场景下,componentDidMount 并不意味着实在的界面已绘制结束。因为机器的性能所限,视图可能还在绘制中。

组件更新阶段

componentWillReceiveProps

该办法在后续的版本曾经标记弃用,被getDerivedStateFromProps办法代替。在早起的版本这个办法还是有用的,有用的起因是在很多人其实并没有很明确这个办法到底由什么触发:

  1. 当父组件批改传递给子组件的属性时,这个批改会带动子组件的对于属性的批改,触发componentWillReceiveProps生命周期。
  2. 当父组件触发了个子组件无关的属性也会触发子组件的componentWillReceiveProps,这阐明componentWillReceiveProps办法的触发不肯定都是因为父组件传递给子组件的属性扭转而引入的。

shouldComponentUpdate

在更新的过程中,会触发render办法来生成新的虚构DOM,进行diff找出须要批改的DOM。这个过程是很消耗工夫的。在实际操作中,咱们会无心触发render办法,为了防止不必要的render调用带来的性能耗费,所以React让咱们能够在shouldComponent办法决定是否要执行余下的申明周期,默认它是返回true。咱们也能够手动设置false,不进行余下的生命周期。

componentWillUpdate

在render函数之前执行,运行做一些不波及实在DOM的操作。后续版本曾经被废除。

render

和挂载阶段统一

componentDidUpdate

在render函数之后执行,DOM曾经更新实现。这个生命周期也常常被用来解决 DOM 操作。此外,咱们也经常将 componentDidUpdate 的执行作为子组件更新结束的标记告诉到父组件。

组件销毁

componentWillUnmount

组件卸载之前触发的生命周期,该函数次要用于执行清理工作。一个比拟常见的 Bug 就是遗记在 componentWillUnmount 中勾销定时器,导致定时操作仍然在组件销毁后不停地执行。所以肯定要在该阶段解除事件绑定,勾销定时器。在平时写代码的时候如果不解除事件绑定和定时器可能会带来动向不想的问题。

componentWillUnmount会在两种状况下触发

  • 组件在父组件中被移除(销毁)
  • 组件设置了KEY属性,父组件在re-render的时候发现key和上一次不统一了就会被移除

React 16生命周期

对于React16x版本的生命周期能够分为两个版本16.3和>=16.4。有一位大神弄了一个在线查看React生命周期的网页,有兴致的同学可看看,地址。

生命周期测试例子),测试版本React 16.3.0

组件的初始化渲染(挂载)

隐没的 componentWillMount,新增的 getDerivedStateFromProps
留神这个的getDerivedSatateFromProps不是componentWillMount的替代品 。getRerivedSatateFromProps设计的初衷是为了代替componentWillReceiveProps。然而说用来代替componentWillReceiveProps也不是完全正确。具体的起因我会在后续阐明。
  • getDerivedStateFromProps是一个静态方法,不依赖实例存储,所以在getDerivedStateFromProps办法内是拜访不到this的。
static getDerivedStateFromProps(props, state) {    this.xxx //  this -> null}
  • getDerivedStateFromProps承受两个参数,第一个参数是承受来自父组件的props,第二参数是以后组件自生的state。
  • getDerivedStateFromProps须要一个对象格局的返回值,如果你没有返回值,React会收回正告。

  • getReriverSatateFromProps的返回值之所以不可或缺,是因为React须要应用这个返回值来更新组件的state。因而当你的确不存在“应用 props 派生state”这个需要的时候,最好是间接省略掉这个生命周期办法的编写,否则肯定记得给它 return 一个 null。

  • 留神getDerivedStateFromProps对state的更新动作不是笼罩式,是针对性的更新。
其余的生命周期

16版本和15版本在挂载阶段的其余生命周期一模一样的,这里就不过多的论述了。

组件更新阶段

getRerivedStateFromProps
React16.3和>=React16.4版本差别在哪?

React16.3和>=React16.4版本生命周期在加载和卸载都是一样的,差别就在更新阶段。在React16.4中,任何因素触发的组件的更新(包含this.setState和forceUpdate触发的更新流程)都会触发getRerivedStateFromProps,在React.16.3只有在父组件更新是会触发getRerivedStateFromProps。

这里请记住,在不同版本getRerivedStateFromProps办法的触发源点可能不同。

为什么要用getRerivedStateFromProps代替componentWillReceivedProps?

其实getRerivedStateProps并不能齐全代替componentWillReceivedProps,而是保障了这个办法的单一性,相对来说是在做一个正当的减法。getRerivedSatetFromProps办法是一个静态方法,是拿不到组件实例的this,这就导致你无奈咋在这个办法内做this.fetch、不合理的this.setState,这类副作用的操作。

因而,getDerivedStateFromProps 生命周期代替 componentWillReceiveProps 的背地,是 React 16 在强制推广『只用 getDerivedStateFromProps 来实现 props 到 state 的映射』这一最佳实际。意在确保生命周期函数的行为更加可控可预测,从本源上帮开发者防止不合理的编程形式,防止生命周期的滥用;

getSnapshotBeforeUpdate
// 组件更新时调用  getSnapshotBeforeUpdate(prevProps, prevState) {    console.log("getSnapshotBeforeUpdate办法执行");    return "haha";  }  // 组件更新后调用  componentDidUpdate(prevProps, prevState, valueFromSnapshot) {    console.log("componentDidUpdate办法执行");    console.log("从 getSnapshotBeforeUpdate 获取到的值是", valueFromSnapshot);  }

  • getSnapshotBeforeUpdate是render办法之后,DOM更新之前执行。
  • getSnapshotBeforeUpdate的返回值会作为componentDidUpdate的第三个参数。
  • getSnapshotBeforeUpdate能够获取到跟新之前的DOM和更新之后的state、pprops。

组件销毁

16版本销毁阶段和15无差别。如果有不理解,请回看15版本申明周期的销毁阶段。

React15 和 React16的实质差异

在16版本开始React引入了Fiber架构。Fiber是React 16 对React外围算法的一次重写。Fiber的外围就是本来的同步渲染变成了异步渲染。

Fiber的初衷

Fiber的初衷就是解决React 15版本中JS无管制的长期占用主线程导致白屏、卡顿等状况。JavaScript在浏览器的主线程上运行,恰好与款式计算、布局以及许多状况下的绘制一起运行。如果JavaScript运行工夫过长,就会阻塞这些其余工作,也可能导致掉帧。

Fiber外围指标

  • 把可中断的工作拆分成小工作
  • 对正在做的工作调整优先秩序、重做、复用上次
  • 在父子工作之间从容切换(yield back and forth),以反对React执行过程中的布局刷新
  • 反对render()返回多个元素
  • 更好地反对error boundary

没有Fiber架构

在React16之前,每当组件更新时,都会生成一个新的虚构DOM。而后和上一次的虚构DOM进行diff,找出差别实现更新。这个过程是一个递归的过程。只有没有到最初一步,就会始终递归,最可怕的是,这是一个串行的过程,可想而知这有如许恐怖。

同步渲染的递归调用栈是十分深的,只有底层调用返回了,这个渲染过程才会逐层返回。然而同步的过程中,会导致主线程不能做其余事件,直到递归实现,还有就是如果这个递归渲染的工夫过程,会造成页面的卡顿或者卡死。

有Fiber架构

在React16之后,引入了Fiber架构,Fiber将一个大的更新工作拆分成很多小的工作,每一个小的工作实现字后,渲染线程会把主线程交还回去,看看有没有优先级更高的工作须要解决,从而防止卡顿的状况。在整个过程中,线程不在是一去不回头的状态了,而是能够被打断的,这就是所谓的"异步渲染"。

换一个角度来看生命周期工作流

在下面说到React16之后的一个重大改革就是引入了Fiber架构,整个架构吧同步渲染变成了异步渲染,在异步渲染的过程中,这个异步是能够被"打断"的,然而留神"打断"是有准则的。

什么时候能够被打断?


如图,在React16中,生命周期被划分成两个阶段,render和commit。commit又能够细分为pre-commmit和commit。

  • render阶段:污浊没有副作用,能够暂停、重启、终止。
  • pre-commit阶段:能够读取DOM.
  • commit阶段:能够应用DOM,运行副作用,安顿更新。

在render阶段是能够被打断的,而commit阶段是不能够被打断的,同步执行。

Why? render阶段能够被打断,commit阶段就不行了

因为render阶段的操作对用户是不可见的,无论怎么操作对用户来说都是零感知,然而commit阶段波及到实在DOM的渲染,如果在用户眼皮下胡乱的更改视图,哪也太胆大包天了。

为什么要变更生命周期?

咱们在回过头在想想为什么在React要"废旧立新"。在React16废除了:

  • componentWillMount
  • componentWillUpdate
  • componentReceiveProps

这三个生命周期,这三个生命周期的共性就是出于render阶段,都可能被反复执行。反复执行的过程可能有很多危险:

  • componentWillxxx办法的异步申请可能被触发屡次
  • componentWillxxx办法外面滥用setState导致反复渲染呈现死循环。

所以,React16革新生命周期的次要动机就是配合Fiber架构带来的异步渲染。在革新的过程中,针对生命周期中长期被滥用的局部推广了具备强制性的最佳实际。

生命周期的那些坑坑洼洼

下面介绍了在不同版本的生命周期,那在生命周期中有那些坑了。开篇提到,呈现坑就是在:

  • 在不失当的机会调用了不适合的代码
  • 在须要调用的时候,没有调用

那防止这些坑就是

  • 不在失当的机会调用不适合的代码
  • 在须要调用的时候,去正确调用。

函数组件的有效触发

函数组件是一种无状态的组件,无生命周期,它在任何状况下都会被触发。看个[例子]

import React from "react";import ReactDom from "react-dom";function TestComponent(props) {  console.log("我从新渲染了");  return <div>函数组件:1</div>;}class LifeCycle extends React.Component {  constructor(props) {    super(props);    this.state = {      text: "这个子组件文本",      message: "这个是子组件音讯"    };  }  changeText = () => {    this.setState({      text: "批改后的子组件文本"    });  };  changeMessage = () => {    this.setState({      text: "批改后的子组件音讯"    });  };  render() {    return (      <div className="container">        <button onClick={this.changeText} className="changeText">          批改子组件文本内容        </button>        <button onClick={this.changeMessage} className="changeText">          批改子组件音讯        </button>        <p>{this.state.text}</p>        <p>{this.state.message}</p>        <p className="textContent">          <TestComponent />        </p>      </div>    );  }}class LifeCycelContainer extends React.Component {  state = {    text: "父组件文本",    message: "父组件音讯"  };  changeText = () => {    this.setState({      text: "批改后的父组件文本"    });  };  changeMessage = () => {    this.setState({      message: "批改后的父组件音讯"    });  };  render() {    return (      <div className="fatherContainer">        <button onClick={this.changeText} className="changeText">          批改父组件文本        </button>        <button onClick={this.changeText} className="changeText">          批改父组件音讯        </button>        <p>{this.state.text}</p>        <p>{this.state.message}</p>        <LifeCycle />      </div>    );  }}ReactDom.render(<LifeCycelContainer />, document.getElementById("root"));

函数组件任何状况下都会从新渲染。它并没有生命周期,但官网提供了一种形式优化伎俩,那就是 React.memo。

const MyComponent = React.memo(function MyComponent(props) {  console.log("memo: 我从新渲染了");  return <div>memo函数组件:2</div>;});

React.memo 并不是阻断渲染,而是跳过渲染组件的操作并间接复用最近一次渲染的后果,这与 shouldComponentUpdate 是齐全不同的。

React.Component的有效触发

定义shouldComponentUpdate函数来防止有效的触发

shouldComponentUpdate() {    // xxxx    return false;}

React.PureComponent审慎应用

class LifeComponent extends PureComponent{    render(){        return (            <p>{this.props.title}</p>        )    }}

React.PureComponent 与 React.Component 简直完全相同,但 React.PureComponent 通过prop和state的浅比照来实现 shouldComponentUpate()。

如果React组件的 render() 函数在给定雷同的props和state下渲染为雷同的后果,在某些场景下你能够应用 React.PureComponent 来晋升性能。

React.PureComponent 的 shouldComponentUpdate() 只会对对象进行浅比照。如果对象蕴含简单的数据结构,它可能会因深层的数据不统一而产生谬误的否定判断(体现为对象深层的数据已扭转视图却没有更新, 原文:false-negatives)。当你冀望只领有简略的props和state时,才去继承 PureComponent ,或者在你晓得深层的数据结构曾经产生扭转时应用 forceUpate() 。或者,思考应用 不可变对象 来促成嵌套数据的疾速比拟。

componentWillMount

componentWillMount 在 React 中已被标记弃用,不举荐应用,次要起因是新的异步渲染架构会导致它被屡次调用。所以网络申请及事件绑定代码应移至 componentDidMount 中。

componentWillMount在页面初始化render之前会执行一次或屡次(async rendering)。

很多同学在此生命周期进行申请数据想放慢首页的渲染速度,然而因为JavaScript中异步事件的性质,当您启动API调用时,浏览器会在此期间返回执行其余工作。当React渲染一个组件时,它不会期待componentWillMount它实现任何事件,React继续前进并持续render,没有方法“暂停”渲染以期待数据达到。componentDidMount操作更加适合做这些操作。

componentWillReceiveProps

componentWillReceiveProps被标记弃用,新版应用 getDerivedStateFromProps 取代,一方面是性能问题,另一方面是从根本上实现代码的最优解,防止副作用。

componentWillUnmount

记得在 componentWillUnmount 函数中去解决解除事件绑定,勾销定时器等清理操作,免得引起不必要的bug。

增加边界解决

默认状况下,若一个组件在渲染期间(render)产生谬误,会导致整个组件树全副被卸载。谬误边界:是一个组件,该组件会捕捉到渲染期间(render)子组件产生的谬误,并有能力阻止谬误持续流传。

谬误边界是一种 React 组件,这种组件能够捕捉并打印产生在其子组件树任何地位的 JavaScript 谬误,并且,它会渲染出备用 UI,而不是渲染那些解体了的子组件树。谬误边界在渲染期间、生命周期办法和整个组件树的构造函数中捕捉谬误。

留神,谬误边界无奈捕捉以下场景中产生的谬误:1、事件处理。2、异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)。3、服务端渲染。4、它本身抛出来的谬误(并非它的子组件)。

如果一个 class 组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期办法中的任意一个(或两个)时,那么它就变成一个谬误边界。当抛出谬误后,请应用 static getDerivedStateFromError() 渲染备用 UI ,应用 componentDidCatch() 打印错误信息。

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;   }}

代码起源

谬误边界的工作形式相似于 JavaScript 的 catch {},不同的中央在于谬误边界只针对 React 组件。只有 class 组件才能够成为谬误边界组件。大多数状况下, 你只须要申明一次谬误边界组件, 并在整个利用中应用它。

留神谬误边界仅能够捕捉其子组件的谬误,它无奈捕捉其本身的谬误。如果一个谬误边界无奈渲染错误信息,则谬误会冒泡至最近的下层谬误边界,这也相似于 JavaScript 中 catch {} 的工作机制。

边界解决的例子

总结

在日常的开发中可能你会遇到这样那样的坑,这里可能只是一些。也可能有些同学说我用的是React hooks,更基本没有这些生命周期,其实不论有没有用其实大抵相似,只是模式不一样了。心愿对大家有用,也心愿更多的同学说出你遇到的坑,大家一起学习!skr~~~~