关于react.js:深入-React-的-setState-机制

10次阅读

共计 9114 个字符,预计需要花费 23 分钟才能阅读完成。

前言

本篇写的 setState(波及源码局部)是针对 React15 版本,即是没有 Fiber 染指的;为了不便看和写,所以抉择旧版本,Fiber 写起来有点难,先留着将会写。setState 在 React 15 的原理能了解,16 版本的也是大同小异。

尽管曾经用 React Hooks 很久了,React15 的 this.setState() 模式都很少用了,但仍然是站在回顾与总结的角度,对待 React 的变迁和倒退,所以最近开始从新回顾以前只知其一; 不知其二的一些原理问题,缓缓积淀技术。

setState 经典问题

setState(updater, [callback])

React 通过 this.setState() 来更新 state,当应用 this.setState()的时候,React 会调用 render 办法来从新渲染 UI。

setState 的几种用法就不必我说了,来看看网上探讨 setState 比拟多的问题:

批量更新

import React, {Component} from 'react'

class App extends Component {
  state = {count: 1,}

  handleClick = () => {
    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 1

    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 1

    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 1
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}> 加 1 </button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

点击按钮触发事件,打印的都是 1,页面显示 count 的值为 2。

这就是经常说的 setState 批量更新,对同一个值进行屡次 setState,setState 的批量更新策略会对其进行笼罩,取最初一次的执行后果。所以每次 setState 之后立刻打印值都是初始值 1,而最初页面显示的值则为最初一次的执行后果,也就是 2。

setTimeout

import React, {Component} from 'react'

class App extends Component {
  state = {count: 1,}

  handleClick = () => {
    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 1

    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 1

    setTimeout(() => {
      this.setState({count: this.state.count + 1,})
      console.log(this.state.count) // 3
    })
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}> 加 1 </button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

点击按钮触发事件,发现 setTimeout 外面的 count 值打印值为 3,页面显示 count 的值为 3。setTimeout 外面 setState 之后能马上能到最新值。

在 setTimeout 外面,setState 是同步的;通过后面两次的 setState 批量更新,count 值曾经更新为 2。在 setTimeout 外面的首先拿到新的 count 值 2,再一次 setState,而后能实时拿到 count 的值为 3。

DOM 原生事件

import React, {Component} from 'react'

class App extends Component {
  state = {count: 1,}

  componentDidMount() {document.getElementById('btn').addEventListener('click', this.handleClick)
  }

  handleClick = () => {
    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 2

    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 3

    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 4
  }

  render() {
    return (
      <>
        <button id='btn'> 触发原生事件 </button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

点击按钮,会发现每次 setState 打印进去的值都是实时拿到的,不会进行批量更新。

在 DOM 原生事件外面,setState 也是同步的。

setState 同步异步问题

这里探讨的同步和异步并不是指 setState 是否异步执行,应用了什么异步代码,而是指调用 setState 之后 this.state 是否立刻更新。

React 中的事件都是合成事件,都是由 React 外部封装好的。React 自身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用程序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,就是咱们所说的 ” 异步 ” 了。

由下面也能够得悉 setState 在原生事件和 setTimeout 中都是同步的。

setState 源码层面

源码抉择的 React 版本为15.6.2

setState 函数

源码外面,setState 函数的代码

React 组件继承自 React.Component,而 setState 是React.Component 的办法

ReactComponent.prototype.setState = function (partialState, callback) {this.updater.enqueueSetState(this, partialState)
  if (callback) {this.updater.enqueueCallback(this, callback, 'setState')
  }
}

能够看到它间接调用了 this.updater.enqueueSetState 这个办法。

enqueueSetState

enqueueSetState: function(publicInstance, partialState) {
  // 拿到对应的组件实例
  var internalInstance = getInternalInstanceReadyForUpdate(
    publicInstance,
    'setState',
  );
  // queue 对应一个组件实例的 state 数组
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState); // 将 partialState 放入待更新 state 队列
  // 解决以后的组件实例
  enqueueUpdate(internalInstance);
}

_pendingStateQueue示意待更新队列

enqueueSetState 做了两件事:

  • 将新的 state 放进组件的状态队列里;
  • 用 enqueueUpdate 来解决将要更新的实例对象。

接下来看看 enqueueUpdate 做了什么:

function enqueueUpdate(component) {ensureInjected()
  // isBatchingUpdates 标识着以后是否处于批量更新过程
  if (!batchingStrategy.isBatchingUpdates) {
    // 若以后没有处于批量创立 / 更新组件的阶段,则立刻更新组件
    batchingStrategy.batchedUpdates(enqueueUpdate, component)
    return
  }
  // 须要批量更新,则先把组件塞入 dirtyComponents 队列
  dirtyComponents.push(component)
  if (component._updateBatchNumber == null) {component._updateBatchNumber = updateBatchNumber + 1}
}

batchingStrategy 示意批量更新策略,isBatchingUpdates示意以后是否处于批量更新过程,默认是 false。

enqueueUpdate做的事件:

  • 判断组件是否处于批量更新模式,如果是,即 isBatchingUpdates 为 true 时,不进行 state 的更新操作,而是将须要更新的组件增加到 dirtyComponents 数组中;
  • 如果不是处于批量更新模式,则对所有队列中的更新执行 batchedUpdates 办法

当中 batchingStrategy该对象的 isBatchingUpdates 属性间接决定了是马上要走更新流程,还是应该进入队列期待;所以大略能够得悉 batchingStrategy 用于管控批量更新的对象。

来看看它的源码:

/**
 *  batchingStrategy 源码
 **/
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false, // 初始值为 false 示意以后并未进行任何批量更新操作

  // 发动更新动作的办法
  batchedUpdates: function (callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates

    ReactDefaultBatchingStrategy.isBatchingUpdates = true

    if (alreadyBatchingUpdates) {return callback(a, b, c, d, e)
    } else {
      // 启动事务,将 callback 放进事务里执行
      return transaction.perform(callback, null, a, b, c, d, e)
    }
  },
}

每当 React 调用 batchedUpdate 去执行更新动作时,会先把 isBatchingUpdates 置为 true,表明正处于批量更新过程中。

看完批量更新整体的管理机制,发现还有一个操作是transaction.perform,这就引出 React 中的 Transaction(事务)机制。

Transaction(事务)机制

Transaction 是创立一个黑盒,该黑盒可能封装任何的办法。因而,那些须要在函数运行前、后运行的办法能够通过此办法封装(即便函数运行中有异样抛出,这些固定的办法仍可运行)。

在 React 中源码有对于 Transaction 的正文如下:

 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>

依据以上正文,能够看出:一个 Transaction 就是将须要执行的 method 应用 wrapper(一组 initialize 及 close 办法称为一个 wrapper)封装起来,再通过 Transaction 提供的 perform 办法执行。

在 perform 之前,先执行所有 wrapper 中的 initialize 办法;perform 实现之后(即 method 执行后)再执行所有的 close 办法,而且 Transaction 反对多个 wrapper 叠加。这就是 React 中的事务机制。

batchingStrategy 批量更新策略

再看回 batchingStrategy 批量更新策略,ReactDefaultBatchingStrategy 其实就是一个批量更新策略事务,它的 wrapper 有两个:FLUSH_BATCHED_UPDATESRESET_BATCHED_UPDATES

isBatchingUpdates在 close 办法被复位为 false,如下代码:

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {ReactDefaultBatchingStrategy.isBatchingUpdates = false},
}
//  flushBatchedUpdates 将所有的长期 state 合并并计算出最新的 props 及 state
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
}

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES]

React 钩子函数

都说 React 钩子函数也是异步更新,则必须一开始 isBatchingUpdates 为 ture,但默认 isBatchingUpdates 为 false,它是在哪里被设置为 true 的呢?来看上面代码:

// ReactMount.js
_renderNewRootComponent: function(nextElement, container, shouldReuseMarkup, context) {
  // 实例化组件
  var componentInstance = instantiateReactComponent(nextElement);
  // 调用 batchedUpdates 办法
  ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,
    componentInstance,
    container,
    shouldReuseMarkup,
    context
  );
}

这段代码是在首次渲染组件时会执行的一个办法,能够看到它外部调用了一次 batchedUpdates 办法(将 isBatchingUpdates 设为 true),这是因为在组件的渲染过程中,会依照顺序调用各个生命周期 (钩子) 函数。如果在函数外面调用 setState,则看下列代码:

if (!batchingStrategy.isBatchingUpdates) {
  // 立刻更新组件
  batchingStrategy.batchedUpdates(enqueueUpdate, component)
  return
}
// 批量更新,则先把组件塞入 dirtyComponents 队列
dirtyComponents.push(component)

则所有的更新都可能进入 dirtyComponents 里去,即 setState 走的异步更新

React 合成事件

当咱们在组件上绑定了事件之后,事件中也有可能会触发 setState。为了确保每一次 setState 都无效,React 同样会在此处手动开启批量更新。看上面代码:

// ReactEventListener.js

dispatchEvent: function (topLevelType, nativeEvent) {
  try {
    // 处理事件:batchedUpdates 会将 isBatchingUpdates 设为 true
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  } finally {TopLevelCallbackBookKeeping.release(bookKeeping);
  }
}

isBatchingUpdates 这个变量,在 React 的生命周期函数以及合成事件执行前,曾经被 React 改为 true,这时咱们所做的 setState 操作天然不会立刻失效。当函数执行结束后,事务的 close 办法会再把 isBatchingUpdates 改为 false。

就像最下面的例子,整个过程模仿大略是:

handleClick = () => {
  // isBatchingUpdates = true
  this.setState({count: this.state.count + 1,})
  console.log(this.state.count) // 1

  this.setState({count: this.state.count + 1,})
  console.log(this.state.count) // 1

  this.setState({count: this.state.count + 1,})
  console.log(this.state.count) // 1
  // isBatchingUpdates = false
}

而如果有 setTimeout 染指后

handleClick = () => {
  // isBatchingUpdates = true
  this.setState({count: this.state.count + 1,})
  console.log(this.state.count) // 1

  this.setState({count: this.state.count + 1,})
  console.log(this.state.count) // 1

  setTimeout(() => {
    // setTimeout 异步执行,此时 isBatchingUpdates 曾经被重置为 false
    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 3
  })
  // isBatchingUpdates = false
}

isBatchingUpdates是在同步代码中变动的,而 setTimeout 的逻辑是异步执行的。当 this.setState 调用真正产生的时候,isBatchingUpdates 早曾经被重置为 false,这就使得 setTimeout 外面的 setState 具备了立即发动同步更新的能力。

batchedUpdates 办法

看到这里大略就能够理解 setState 的同步异步机制了,接下来让咱们进一步领会,能够把 React 的 batchedUpdates 拿来试试,在该版本中此办法名称被置为 unstable_batchedUpdates 即不稳固的办法。

import React, {Component} from 'react'
import {unstable_batchedUpdates as batchedUpdates} from 'react-dom'

class App extends Component {
  state = {count: 1,}

  handleClick = () => {
    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 1

    this.setState({count: this.state.count + 1,})
    console.log(this.state.count) // 1

    setTimeout(() => {batchedUpdates(() => {
        this.setState({count: this.state.count + 1,})
        console.log(this.state.count) // 2
      })
    })
  }
  render() {
    return (
      <>
        <button onClick={this.handleClick}> 加 1 </button>
        <div>{this.state.count}</div>
      </>
    )
  }
}

export default App

如果调用 batchedUpdates 办法,则 isBatchingUpdates变量会被设置为 true,由上述得为 true 走的是批量更新策略,则 setTimeout 外面的办法也变成异步更新了,所以最终打印值为 2,与本文第一道题后果一样。

总结

setState 同步异步的体现会因调用场景的不同而不同:在 React 钩子函数及合成事件中,它体现为异步;而在 setTimeout/setInterval 函数,DOM 原生事件中,它都体现为同步。这是由 React 事务机制和批量更新机制的工作形式来决定的。

在 React16 中,因为引入了 Fiber 机制,源码多少有点不同,但大同小异,之后我也会写 React16 原理的文章,敬请关注!

我近期会保护的开源我的项目:

  • 基于 React + TypeScript + Dumi + Jest + Enzyme 开发 UI 组件库
  • Next.js 企业级我的项目脚手架模板
  • 集体技术博文 Github 仓库
    感觉不错的话欢送 star,给我一点激励持续写作吧~
正文完
 0