[转]深入浅出 React -- 生命周期

[转]深入浅出 React -- 生命周期
这里通过对 React15 和 React16 两个版本的生命周期进行对比总结,来建立系统而完善的生命周期知识体系.

生命周期背后的设计思想

React 设计的两个核心概念:“组件” 和 “虚拟 DOM”

虚拟 DOM

当组件初始化时,通过调用生命周期中的 render 方法,生成虚拟 DOM;再通过调用 ReactDOM.render 方法,将虚拟 DOM 转换为真实 DOM。

当组件更新时,会再次调用生命周期中的 render 方法,生成新的虚拟 DOM;然后通过 diff 算法定位两次虚拟 DOM 的差异,对发生变化的真实 DOM 做定向更新。

组件化

在一个 React 项目中,几乎所有的内容都可以抽离为各种各样的组件,每个组件既是 “封闭” 的,也是 “开放” 的。

所谓 “封闭”,是针对组件数据改变到组件实际发生更新的过程。在组件自身的渲染过程中,每个组件都只会处理它自身内部的渲染逻辑。在没有数据交流的情况下,组件之间互不干扰。

所谓 “开放”,是针对组件间通信的。React 允许开发者基于单向数据流的原则来完成组件之间的通信。组件之间的通信可能使通信组件的渲染结果产生影响。所以说组件之间是相互开放的,可以相互影响的。

React 组件的 “开放” 与 “封闭” 特性,使得 React 的组件具备高可重用性和可维护性。

生命周期方法

生命周期的 render 方法将虚拟 DOM组件两者结合到了一起。

虚拟 DOM 的生成依赖 render,而组件的渲染过程也离不开 render。所以可以将 render 方法比作组件的“灵魂”

render 之外的生命周期方法可以理解为组件的“躯干”

我们可以省略 render 之外的任何生命周期方法内容的编写,但是 render 函数不能省略;但是 render 之外的生命周期方法的编写,通常是为 render 服务;“灵魂” 和 “躯干” 共同构成了 React 组件完整的生命时间轴。

React15 生命周期

在 React15 中,需要关注以下生命周期方法:

constructor()
componentWillReceiveProps()
shouldComponentUpdate()
componentWillMount()
componentWillUpdate()
componentDidUpdate()
componentDidMount()
render()
componentWillUnmount()

这些生命周期方法的关系:


lifecycle

下面的示例可以验证:

import React from "react"
import ReactDOM from "react-dom"

// 代码源自 “深入浅出搞定 React -- 修言”

// 定义子组件
class LifeCycle extends React.Component {
  constructor(props) {
    console.log("进入constructor")

    super(props)

    // state 可以在 constructor 里初始化
    this.state = { text: "子组件的文本" }
  }
  
  // 初始化渲染时调用
  componentWillMount() {
    console.log("componentWillMount方法执行")
  }

// 初始化渲染时调用
  componentDidMount() {
    console.log("componentDidMount方法执行")
  }

  // 父组件修改组件的props时会调用
  componentWillReceiveProps(nextProps) {
    console.log("componentWillReceiveProps方法执行")
  }

  // 组件更新时调用
  shouldComponentUpdate(nextProps, nextState) {
    console.log("shouldComponentUpdate方法执行")
    return true
  }

// 组件更新时调用
  componentWillUpdate(nextProps, nextState) {
    console.log("componentWillUpdate方法执行")
  }

  // 组件更新后调用
  componentDidUpdate(nextProps, nextState) {
    console.log("componentDidUpdate方法执行")
  }

  // 组件卸载时调用
  componentWillUnmount() {
    console.log("子组件的componentWillUnmount方法执行")
  }

// 点击按钮,修改子组件文本内容的方法
  changeText = () => {
    this.setState({
      text: "修改后的子组件文本"
    })
  }

  render() {
    console.log("render方法执行")
    return (
      <div className="container">
        <button onClick={this.changeText} className="changeText">
          修改子组件文本内容
        </button>
        <p className="textContent">{this.state.text}</p>
        <p className="fatherContent">{this.props.text}</p>
      </div>
    )
  }
}

// 定义 LifeCycle 组件的父组件
class LifeCycleContainer extends React.Component {
  // state 也可以像这样用属性声明的形式初始化
  state = {
    text: "父组件的文本",
    hideChild: false
  }
  // 点击按钮,修改父组件文本的方法
  changeText = () => {
    this.setState({
      text: "修改后的父组件文本"
    })
  }

 // 点击按钮,隐藏(卸载)LifeCycle 组件的方法
  hideChild = () => {
    this.setState({
      hideChild: true
    })
  }

render() {
    return (
      <div className="fatherContainer">
        <button onClick={this.changeText} className="changeText">
          修改父组件文本内容
        </button>
        <button onClick={this.hideChild} className="hideChild">
          隐藏子组件
        </button>
        {this.state.hideChild ? null : <LifeCycle text={this.state.text} />}
      </div>
    )
  }
}

ReactDOM.render(<LifeCycleContainer />, document.getElementById("root"))

挂载阶段

组件挂载在一个 React 组件的生命周期中只会发生一次,在这个过程中,组件被初始化,最后被渲染到真实 DOM;

挂载阶段,一个 React 组件所经历的生命周期:


load
  • constructor():对 this.state 初始化。
  • componentWillMount() :在 render 方法前被触发。
  • render() :生成需要渲染的内容并返回,不会操作真实 DOM。真实 DOM 的渲染由 ReactDOM.render 完成。
  • componentDidMount() :在渲染结束后被触发,此时可以访问真实 DOM 。在这个生命周期中也可以做类似于异步请求、数据初始化的操作。

更新阶段

更新阶段,一个 React 组件所经历的生命周期:


update

componentWillReceiveProps

从图中可以看出,由父组件触发的更新和由组件自身触发的更新对比,多出了一个生命周期方法:componentWillReceiveProps(nextProps)

nextProps 表示新 props 内容,而现有的 props 可以通过 this.props 获取,从而对比 props 的变化。

如果父组件导致组件重新渲染,即使 props 没有更改,也会调用此方法(componentWillReceiveProps)。如果只想处理更改,请确保进行当前值与变更值的比较。

componentWillReceiveProps 并不是由 props 的变化触发的,而是由父组件的更新触发的

shouldComponentUpdate

shouldComponentUpdate(nextProps, nextState)

由于 render 方法会进行虚拟 DOM 的构建和对比,比较耗时。为了避免不必要的 render 调用,React 提供了 shouldComponentUpdate 生命周期方法。

根据 shouldComponentUpdate() 的返回值,判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染。大部分情况下,你应该遵循默认行为。

此方法仅作为性能优化的方式而存在。不要企图依靠此方法来“阻止”渲染,因为这可能会产生 bug。你应该考虑使用内置的 PureComponent 组件,而不是手动编写 shouldComponentUpdate()PureComponent 会对 props 和 state 进行浅层比较,并减少了跳过必要更新的可能性。

componentWillUpdate 和 componentDidUpdate

componentWillUpdaterender 前触发,和 componentWillMount类似,可以在里面做一些与真实 DOM 不相关的操作。
componentDidUpdate 在组件更新完成后触发,和 componentDidMount 类似,可以在里面处理 DOM 操作;作为子组件更新完毕通知父组件的标志。

卸载阶段

组件销毁,只有 componentWillUnmount() 生命周期,可以在里面做一些释放内存,清理定时器等操作。

React16 生命周期

React 16.3 生命周期:

React16

示例代码:

import React from "react"
import ReactDOM from "react-dom"

// 代码源自 “深入浅出搞定 React -- 修言”

// 定义子组件
class LifeCycle extends React.Component {
  constructor(props) {
    console.log("进入constructor")

    super(props)

    // state 可以在 constructor 里初始化
    this.state = { text: "子组件的文本" }
  }

// 初始化/更新时调用
  static getDerivedStateFromProps(props, state) {
    console.log("getDerivedStateFromProps方法执行")
    return {
      fatherText: props.text
    }
  }

  // 初始化渲染时调用
  componentDidMount() {
    console.log("componentDidMount方法执行")
  }

  // 组件更新时调用
  shouldComponentUpdate(prevProps, nextState) {
    console.log("shouldComponentUpdate方法执行")
    return true
  }

// 组件更新时调用
  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log("getSnapshotBeforeUpdate方法执行")
    return "haha"
  }

  // 组件更新后调用
  componentDidUpdate(nextProps, nextState, valueFromSnapshot) {
    console.log("componentDidUpdate方法执行")
    console.log("从 getSnapshotBeforeUpdate 获取到的值是", valueFromSnapshot)
  }

  // 组件卸载时调用
  componentWillUnmount() {
    console.log("子组件的componentWillUnmount方法执行")
  }

// 点击按钮,修改子组件文本内容的方法
  changeText = () => {
    this.setState({
      text: "修改后的子组件文本"
    })
  }

  render() {
    console.log("render方法执行");
    return (
      <div className="container">
        <button onClick={this.changeText} className="changeText">
          修改子组件文本内容
        </button>
        <p className="textContent">{this.state.text}</p>
        <p className="fatherContent">{this.props.text}</p>
      </div>
    )
  }
}

// 定义 LifeCycle 组件的父组件
class LifeCycleContainer extends React.Component {
  // state 也可以像这样用属性声明的形式初始化
  state = {
    text: "父组件的文本",
    hideChild: false
  }

// 点击按钮,修改父组件文本的方法
  changeText = () => {
    this.setState({
      text: "修改后的父组件文本"
    })
  }

  // 点击按钮,隐藏(卸载)LifeCycle 组件的方法
  hideChild = () => {
    this.setState({
      hideChild: true
    })
  }

render() {
    return (
      <div className="fatherContainer">
        <button onClick={this.changeText} className="changeText">
          修改父组件文本内容
        </button>
        <button onClick={this.hideChild} className="hideChild">
          隐藏子组件
        </button>
        {this.state.hideChild ? null : <LifeCycle text={this.state.text} />}
      </div>
    )
  }
}

ReactDOM.render(<LifeCycleContainer />, document.getElementById("root"))

挂载阶段

LoadCompare

componentWillMount vs getDerivedStateFromProps

对比于 React 15 废弃了 componentWillMount ,新增了 getDerivedStateFromProps

componentWillMount 的存在不仅“鸡肋”而且危险,因此它不值得被“替代”,而应该直接废弃。

getDerivedStateFromProps 的设计初衷是替换 componentWillReceiveProps,它有且仅有一个作用:让组件在 props 变化时派生/更新 state

getDerivedStateFromProps 的方法签名:

static getDerivedStateFromProps(props, state)
  • getDerivedStateFromProps 是一个静态方法;不依赖组件实例;在这个方法里不能访问 this
  • 两个参数:propsstate,分别表示组件接收的来自父组件的 props 和自身的 state
  • 需要一个对象作为返回值;如果没有指定返回值,React 会发出警告;React 需要用这个返回值来更新/派生组件的 stat;如果不需要,最好直接省略这个方法,否则需要返回 null
  • state 的更新不是“覆盖”,而是针对属性的定向更新。

更新阶段

UpdateCompare

React 16.4 的挂载和卸载和 React 16.3 保持一致,更新阶段不同:

React 16.4 生命周期:

React16.4

  • 在 React 16.4 中,任何因素触发的组件更新都会触发 getDerivedStateFromProps
  • 在 React 16.3 中,只有父组件的更新才会触发 getDerivedStateFromProps

getDerivedStateFromProps

  • getDerivedStateFromProps 是为了试图替换 componentWillReceiveProp 而出现的。
  • getDerivedStateFromProps 不能完全等同于 componentWillReceiveProps
    • 代替实现基于 props 派生 state。
    • 原则上,它能且只能做这一件事。

为什么要用 getDerivedStateFromProps替换 componentWillReceiveProps

“合理的减法”
getDerivedStateFromProps 直接被定义为 static方法,使得在其方法内部无法拿到组件实例的 this,也就不能在里面执行类似不合理的 this.setState (可能会导致死循环)这类会产生副作用的操作。

确保生命周期函数的行为可控可预测,从源头上帮助开发者避免不合理的编码,同时也是为新的Fiber 架构铺路。

componentWillUpdate vs getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps, prevState) {
  // ...
}
  • 执行时机在 render 方法之后,真实 DOM 更新之前
  • 可以获得 DOM 更新前后的 stateprops 信息
  • 返回值将作为componentDidUpdate 的第三个参数

在实际编程中很少用到,但也有特殊场景需要。

例如:实现一个内容会发生变化的滚动列表,要求根据滚动列表的内容是否发生变化,来决定是否要记录滚动条的当前位置。

这个例子中要求我们对比更新前后的数据是否发生变化,还需要获取真实的 DOM 位置信息。

componentDidUpdate 配合编程:

// 组件更新时调用
getSnapshotBeforeUpdate(prevProps, prevState) {
  console.log("getSnapshotBeforeUpdate方法执行")
  return "haha"
}

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

getSnapshotBeforeUpdate 的设计初衷是为了 “与 componentDidUpdate 一起,覆盖过时的componentWillUpdate”。

为什么废除 componentWillUpdate,是因为它不适合 Fiber 架构。

卸载阶段

与 React 15 完全一致

React 16 为何做出两次改变

Fiber 架构简析

使 Virtual DOM 可以进行增量式渲染

Fiber 会使原本同步的渲染过程变成异步的

在 React 16 之前,每次组件更新,React 都会构建虚拟 DOM,再与旧虚拟 DOM 对比 diff,最后对真实 DOM 定向更新。

同步调用的调用栈非常深,需要等到递归调用都返回后,整个渲染才算结束。

这个“漫长”的同步渲染过程不可被打断,存在巨大风险;同步渲染一旦开始,会占据主线程,直到彻底完成;在这个过程中,浏览器无法处理其他任务包括用户交互,甚至可能出现卡顿至卡死的风险。

React 16 引入的 Fiber 架构,可以解决这个风险:Fiber 会将一个大的更新任务拆解为多个小任务;每次执行完成一个小任务,渲染线程都会交还主线程给浏览器,然后处理优先级更高的工作,进而避免同步渲染导致的卡顿。

React 渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。

从 Fiber 架构角度看生命周期

Fiber 架构的重要特征就是渲染过程可以被中断。根据这个特征,React 16 的生命周期被划分为 Render 和 Commit 两个阶段,而 Commit 阶段又被细分为 Pre-commit 和 Commit 阶段。

Fiber

  • Render 阶段:纯净且不包含副作用。可能会被 React 暂停,中止或重新启动。
  • Pre-commit 阶段:可以读取 DOM。
  • Commit 阶段:可以使用 DOM,运行副作用,安排更新。

也就是说在 Render 阶段允许被中断,而 Commit 阶段不能。原因很简单,Render 阶段的操作对于用户不可感知,所以中断、重启对于用户而言是不可见的。而 Commit 阶段的操作是对真实 DOM 的渲染,不能随意中断、重渲染。

React 16 “废旧立新”背后的思考

Fiber 架构下,Render 阶段允许被暂停、终止和重启。当一个任务执行一段后被中断,下一次抢回渲染线程时,这个任务会“重复执行一遍整个任务”而不是接着上一次执行的地方。这导致了 Render 阶段的生命周期方法有可能重复执行。

React 16 废弃的生命周期方法:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

这些方法都处于 Render 阶段,而且这些方法常年被滥用,在重复执行的过程中存在很大的风险。

我们的编码中的一些不好的习惯,在 “componentWill” 开头的生命周期里做一些事情:

  • setState()
  • fetch 异步请求
  • 操作真实 DOM
  • ...

这些操作的问题:

  1. 可以转移到其他生命周期(componentDid...)里去做
  2. Fiber 架构下,可能导致非常严重的 Bug
  3. 在 React 15 中也有出现过问题(在 componentWillReceivePropscomponentWillUpdate里滥用 setState 导致重渲染死循环)

总结

  • React 16 改造生命周期的主要原因是为了配合 Fiber 架构带来的异步渲染机制。
  • 针对生命周期中长期被滥用的部分推出了具有强制性的最佳实践。
  • 确保了 Fiber 架构下的数据和视图的安全,以及确保了生命周期方法的行为更加可控、可预测。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,825评论 4 377
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,887评论 2 308
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,425评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,801评论 0 224
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,252评论 3 299
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,089评论 1 226
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,216评论 2 322
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 31,005评论 0 215
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,747评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,883评论 2 255
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,354评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,694评论 3 265
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,406评论 3 246
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,222评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,996评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,242评论 2 287
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 36,017评论 2 281

推荐阅读更多精彩内容