乐趣区

关于前端:避免React生命周期的那些坑坑洼洼

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

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

— 来自:伯约文章

要防止生命周期的坑,就须要先理解 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~~~~

退出移动版