专栏首页前端皮小蛋[第10期] 了解 React setState 运行机制

[第10期] 了解 React setState 运行机制

使用React 的时候, 难免要用到setState , 有一些基础还是需要了解一下。

下面我们就一起看看其中的细节。

先看一个例子

假如有这样一个点击执行累加场景:

this.state = {
  count: 0,
}

incrementCount() {
  this.setState({
    count: this.state.count + 1,
  });
}

handleIncrement = () => {
 this.incrementCount();
 this.incrementCount();
 this.incrementCount();
}

每一次点击, 累加三次,看一下输入:

并没有达到预期的效果,纠正也很简单:

incrementCount() {
  this.setState((prevState) => {
    return {count: prevState.count + 1}
  });
}

再看输出:

setState 的时候, 一个传入了object, 一个传入了更新函数。

区别在于:传入一个更新函数,就可以访问当前状态值。setState调用是 批量处理的,因此可以让更新建立在彼此之上,避免冲突。

那问题来了, 为什么前一种方式就不行呢?带着这个疑问,继续往下看。

setState 为什么不会同步更新组件

进入这个问题之前,我们先回顾一下现在对 setState 的认知:

  • 1.setState 不会立刻改变React组件中state的值.
  • 2.setState 通过触发一次组件的更新来引发重绘.
  • 3.多次 setState 函数调用产生的效果会合并

重绘指的就是引起 React 的更新生命周期函数4个函数:

  • shouldComponentUpdate(被调用时this.state没有更新;如果返回了false,生命周期被中断,虽然不调用之后的函数了,但是state仍然会被更新)
  • componentWillUpdate(被调用时this.state没有更新)
  • render(被调用时this.state得到更新)
  • componentDidUpdate

如果每一次 setState 调用都走一圈生命周期,光是想一想也会觉得会带来性能的问题,其实这四个函数都是纯函数,性能应该还好,但是render函数返回的结果会拿去做Virtual DOM比较和更新DOM树,这个就比较费时间。

目前React会将setState的效果放在队列中,积攒着一次引发更新过程。

为的就是把 Virtual DOM 和 DOM 树操作降到最小,用于提高性能

查阅一些资料后发现,某些操作还是可以同步更新 this.state的。

setState 什么时候会执行同步更新

先直接说结论吧:

在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用 setState 不会同步更新 this.state,除此之外的setState调用会同步执行this.state。

所谓“除此之外”,指的是绕过React通过 addEventListener 直接添加的事件处理函数,还有通过 setTimeout || setInterval

产生的异步调用。

简单一点说, 就是经过React 处理的事件是不会同步更新 this.state的. 通过 addEventListener || setTimeout/setInterval 的方式处理的则会同步更新

具体可以参考 jsBin 的这个例子。

结果就很清晰了:

点击Increment ,执行onClick ,输出0; 而通过addEventListener , 和 setTimeout 方式处理的, 第一次 直接输出了1;

理论大概是这样的,盗用一张图:

在React的setState函数实现中,会根据一个变量 isBatchingUpdates 判断是直接更新 this.state 还是放到队列 中。

isBatchingUpdates 默认是false,也就表示setState会同步更新this.state,但是有一个函数batchedUpdates

这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state。

通过上图,我们知道了大致流程, 要想彻底了解它的机制,我们解读一下源码。

探秘setState 源码

// setState方法入口如下:
ReactComponent.prototype.setState = function (partialState, callback) {
  // 将setState事务放入队列中
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }};
相关的几个概念:
  • partialState,有部分state的含义,可见只是影响涉及到的state,不会伤及无辜。
  • enqueueSetState 是 state 队列管理的入口方法,比较重要,我们之后再接着分析。

replaceState :

replaceState: function (newState, callback) {
  this.updater.enqueueReplaceState(this, newState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'replaceState');
  }},
   replaceState中取名为newState,有完全替换的含义。同样也是以队列的形式来管理的。

enqueueSetState

enqueueSetState: function (publicInstance, partialState) {
    // 先获取ReactComponent组件对象
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

    if (!internalInstance) {
      return;
    }

    // 如果_pendingStateQueue为空,则创建它。可以发现队列是数组形式实现的
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    // 将要更新的ReactComponent放入数组中
    enqueueUpdate(internalInstance);}

其中getInternalInstanceReadyForUpdate源码如下

function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
  // 从map取出ReactComponent组件,还记得mountComponent时把ReactElement作为key,将ReactComponent存入了map中了吧,ReactComponent是React组件的核心,包含各种状态,数据和操作方法。而ReactElement则仅仅是一个数据类。
  var internalInstance = ReactInstanceMap.get(publicInstance);
  if (!internalInstance) {
    return null;
  }

 return internalInstance;}

enqueueUpdate源码如下:

function enqueueUpdate(component) {
  ensureInjected();

  // 如果不是正处于创建或更新组件阶段,则处理update事务
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  // 如果正在创建或更新组件,则暂且先不处理update,只是将组件放在dirtyComponents数组中
  dirtyComponents.push(component);}

batchedUpdates

batchedUpdates: function (callback, a, b, c, d, e) {
  var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
  // 批处理最开始时,将isBatchingUpdates设为true,表明正在更新
  ReactDefaultBatchingStrategy.isBatchingUpdates = true;

  // The code is written this way to avoid extra allocations
  if (alreadyBatchingUpdates) {
    callback(a, b, c, d, e);
  } else {
    // 以事务的方式处理updates,后面详细分析transaction
    transaction.perform(callback, null, a, b, c, d, e);
  }}
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    // 事务批更新处理结束时,将isBatchingUpdates设为了false
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }};var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

enqueueUpdate包含了React避免重复render的逻辑。

mountComponentupdateComponent 方法在执行的最开始,会调用到 batchedUpdates 进行批处理更新,此时会将isBatchingUpdates设置为true,也就是将状态标记为现在正处于更新阶段了。

之后React以事务的方式处理组件update,事务处理完后会调用wrapper.close() 。

TRANSACTION_WRAPPERS 中包含了RESET_BATCHED_UPDATES 这个wrapper,故最终会调用RESET_BATCHED_UPDATES.close(), 它最终会将isBatchingUpdates设置为false

getInitialStatecomponentWillMountrendercomponentWillUpdatesetState 都不会引起 updateComponent

但在componentDidMountcomponentDidUpdate中则会。

事务

事务通过wrapper进行封装。

一个wrapper包含一对 initializeclose 方法。比如 RESET_BATCHED_UPDATES

var RESET_BATCHED_UPDATES = {
  // 初始化调用
  initialize: emptyFunction,
  // 事务执行完成,close时调用
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }};
transcation被包装在wrapper中,比如:
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

transaction 是通过transaction.perform(callback, args…)方法进入的,它会先调用注册好的wrapper 中的initialize方法,然后执行perform方法中的callback,最后再执行close方法。

下面分析transaction.perform(callback, args…)

perform: function (method, scope, a, b, c, d, e, f) {    var errorThrown;
    var ret;
    try {
      this._isInTransaction = true;
      errorThrown = true;
      // 先运行所有wrapper中的initialize方法
      this.initializeAll(0);

      // 再执行perform方法传入的callback
      ret = method.call(scope, a, b, c, d, e, f);
      errorThrown = false;
    } finally {
      try {
        if (errorThrown) {
          // 最后运行wrapper中的close方法
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
          // 最后运行wrapper中的close方法
          this.closeAll(0);
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  },

  initializeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers;
    // 遍历所有注册的wrapper
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
        this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
        // 调用wrapper的initialize方法
        this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
      } finally {
        if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
          try {
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  },

  closeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers;
    // 遍历所有wrapper
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var initData = this.wrapperInitData[i];
      var errorThrown;
      try {
        errorThrown = true;
        if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
          // 调用wrapper的close方法,如果有的话
          wrapper.close.call(this, initData);
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          try {
            this.closeAll(i + 1);
          } catch (e) {}
        }
      }
    }
    this.wrapperInitData.length = 0;
  }

更新组件: runBatchedUpdates

前面分析到enqueueUpdate中调用transaction.perform(callback, args...)后,发现,callback还是enqueueUpdate方法啊,那岂不是死循环了?不是说好的setState会调用updateComponent,从而自动刷新View的吗?我们还是要先从transaction事务说起。

我们的wrapper中注册了两个wrapper,如下:

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

RESET_BATCHED_UPDATES 用来管理isBatchingUpdates状态,我们前面在分析setState是否立即生效时已经讲解过了。

FLUSH_BATCHED_UPDATES用来干嘛呢?

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)};
var flushBatchedUpdates = function () {
  // 循环遍历处理完所有dirtyComponents
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      // close前执行完runBatchedUpdates方法,这是关键
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }};

FLUSH_BATCHED_UPDATES会在一个transactionclose阶段运行runBatchedUpdates,从而执行update

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  dirtyComponents.sort(mountOrderComparator);

  for (var i = 0; i < len; i++) {
    // dirtyComponents中取出一个component
    var component = dirtyComponents[i];

    // 取出dirtyComponent中的未执行的callback,下面就准备执行它了
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;

    var markerName;
    if (ReactFeatureFlags.logTopLevelRenders) {
      var namedComponent = component;
      if (component._currentElement.props === component._renderedComponent._currentElement) {
        namedComponent = component._renderedComponent;
      }
    }
    // 执行updateComponent
    ReactReconciler.performUpdateIfNecessary(component, transaction.reconcileTransaction);

    // 执行dirtyComponent中之前未执行的callback
    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(callbacks[j], component.getPublicInstance());
      }
    }
  }}

runBatchedUpdates循环遍历dirtyComponents数组,主要干两件事。

  1. 首先执行performUpdateIfNecessary来刷新组件的view
  2. 执行之前阻塞的callback。

下面来看performUpdateIfNecessary

performUpdateIfNecessary: function (transaction) {
    if (this._pendingElement != null) {
      // receiveComponent会最终调用到updateComponent,从而刷新View
      ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
    }

    if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      // 执行updateComponent,从而刷新View。这个流程在React生命周期中讲解过
      this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
    }
  },

最后惊喜的看到了receiveComponentupdateComponent吧。

receiveComponent最后会调用updateComponent,而updateComponent中会执行React组件存在期的生命周期方法,

componentWillReceivePropsshouldComponentUpdatecomponentWillUpdaterender, componentDidUpdate

从而完成组件更新的整套流程。

整体流程回顾:

1.enqueueSetState将state放入队列中,并调用enqueueUpdate处理要更新的Component 2.如果组件当前正处于update事务中,则先将Component存入dirtyComponent中。否则调用batchedUpdates处理。 3.batchedUpdates发起一次transaction.perform()事务 4.开始执行事务初始化,运行,结束三个阶段 5.初始化:事务初始化阶段没有注册方法,故无方法要执行 6.运行:执行setSate时传入的callback方法,一般不会传callback参数 7.结束:更新isBatchingUpdates为false,并执行FLUSH_BATCHED_UPDATES这个wrapper中的close方法 8.FLUSH_BATCHED_UPDATES在close阶段,会循环遍历所有的dirtyComponents,调用updateComponent刷新组件,并执行它的pendingCallbacks, 也就是setState中设置的callback。

看完理论, 我们再用一个例子巩固下.

再看一个例子:

class Example extends React.Component {
 constructor() {
   super();
   this.state = {
    val: 0
   };
}
componentDidMount() {
  this.setState({val: this.state.val + 1});
  console.log('第 1 次 log:', this.state.val);
  this.setState({val: this.state.val + 1});
  console.log('第 2 次 log:', this.state.val);

 setTimeout(() => {
  this.setState({val: this.state.val + 1});
  console.log('第 3 次 log:', this.state.val);
  this.setState({val: this.state.val + 1});
  console.log('第 4 次 log:', this.state.val);
 }, 0);
}
 render() {
  return null;
 }
};
前两次在isBatchingUpdates 中,没有更新state, 输出两个0。

后面两次会同步更新, 分别输出2, 3;

很显然,我们可以将4次setState简单分成两类

  1. componentDidMount是一类
  2. setTimeOut中的又是一类,因为这两次在不同的调用栈中执行。

我们先看看在componentDidMount中setState的调用栈:

再看看在setTimeOut中的调用栈:

我们重点看看在componentDidMount中的sw3e调用栈 : 发现了batchedUpdates方法。

原来在setState调用之前,就已经处于batchedUpdates执行的事务之中了。

batchedUpdates方法是谁调用的呢?我们再往上追溯一层,原来是ReactMount.js中的_renderNewRootComponent方法。

也就是说,整个将React组件渲染到DOM的过程就处于一个大的事务中了。

接下来就很容易理解了: 因为在componentDidMount中调用setState时,batchingStrategyisBatchingUpdates已经被设置为true,所以两次setState的结果并没有立即生效,而是被放进了dirtyComponents中。

这也解释了两次打印this.state.val都是0的原因,因为新的state还没被应用到组件中。

再看setTimeOut中的两次setState,因为没有前置的batchedUpdate调用,所以batchingStrategyisBatchingUpdates标志位是false,也就导致了新的state马上生效,没有走到dirtyComponents分支。

也就是说,setTimeOut中的第一次执行,setState时,this.state.val为1;

而setState完成后打印时this.state.val变成了2。

第二次的setState同理。

通过上面的例子,我们就知道setState 是可以同步更新的,但是还是尽量避免直接使用, 仅作了解就可以了。

如果你非要玩一些骚操作,写出这样的代码去直接去操作this.state:

this.state.count = this.state.count + 1;
this.state.count = this.state.count + 1;
this.state.count = this.state.count + 1;
this.setState();

我只能说, 大胸弟, 你很骚。吾有旧友叼似汝,而今坟草丈许高。

结语

最后简单重复下结论吧:

  • 不要直接去操作this.state, 这样会造成不必要的性能问题和隐患。
  • 由React引发的事件处理,调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。

本文分享自微信公众号 - 前端皮小蛋(gh_e69260c16440),作者:南山皮小蛋

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-10-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 第十一篇:setState 到底是同步的,还是异步的?

    setState 对于许多的 React 开发者来说,像是一个“最熟悉的陌生人”:

    越陌度阡
  • react中setState是同步还是异步的

    我们都知道,React框架是由数据来驱动视图变化的,基于状态的管理实现对组件的管理,也就是组件当中的state,通过setState方法来修改当前组件的stat...

    OECOM
  • 【React源码笔记】setState原理解析

    点击上方蓝字,发现更多精彩 导语 大家都知道React是以数据为核心的,当状态发生改变时组件会进行更新并渲染。除了通过React Redux、React Ho...

    腾讯VTeam技术团队
  • setState 到底是同步的,还是异步的

    这是一道变体繁多的面试题,在 BAT 等一线大厂的面试中考察频率非常高。首先题目会给出一个这样的 App 组件,在它的内部会有如下代码所示的几个不同的 setS...

    zz_jesse
  • setState 到底是同步的,还是异步的

    这是一道变体繁多的面试题,在 BAT 等一线大厂的面试中考察频率非常高。首先题目会给出一个这样的 App 组件,在它的内部会有如下代码所示的几个不同的 setS...

    coder_koala
  • 【面试题】1085- setState 到底是同步的,还是异步的

    这是一道变体繁多的面试题,在 BAT 等一线大厂的面试中考察频率非常高。首先题目会给出一个这样的 App 组件,在它的内部会有如下代码所示的几个不同的 setS...

    pingan8787
  • 2020-5-30-理解React如何实现批量状态更新

    在React的生命周期中发生的多次setState的变更会进行合并,最终减少推送给浏览器的DOM变更次数,从而提升前端性能。

    黄腾霄
  • 深入React技术栈之setState详解

    keyWords
  • 深入理解 React setState

    React 修改 state 方法有两种: 1、构造函数里修改 state ,只需要直接操作 this.state 即可, 如果在构造函数里执行了异步操作,就...

    Leophen
  • 由实际问题探究setState的执行机制

    以下几个问题是我们在实际开发中经常会遇到的场景,下面用几个简单的示例代码来还原一下。

    ConardLi
  • 细聊Concent & Recoil , 探索react数据流的新开发模式

    开源不易,感谢你的支持,❤ star me if you like concent ^_^

    腾讯新闻前端团队
  • [第14期] [长文预警] 掌握React 渲染原理及性能优化

    我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家。

    皮小蛋
  • React高频面试题梳理,看看面试怎么答?(上)

    前段时间准备面试,总结了很多,下面是我在准备React面试时,结合自己的实际面试经历,以及我以前源码分析的文章,总结出来的一些 React高频面试题目。

    ConardLi
  • React-setState函数必须掌握的pendingState状态

    查询对应源码内容觉得比较难以理解所以在下方以一个简单Demo记录下setState不同状态下对应实现原理。

    19组清风
  • [长文预警] 一文掌握React 渲染原理及性能优化

    我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家。

    coder_koala
  • 【React】393 深入了解React 渲染原理及性能优化

    我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家。

    pingan8787
  • [第14期] [长文预警] 深入了解React 渲染原理及性能优化

    我比较常用React, 这里就写了一篇 React 基础原理的内容, 面试基本上也就问这些, 分享给大家。

    皮小蛋
  • 使用React和Node.js制作音乐类App的一次总结

    Peter谭金杰
  • React学习(六)-React中组件的数据-state

    一个组件最终渲染的数据结果,除了prop还有state,state代表的是当前组件的内部状态,你可以把组件看成一个'状态机",它是能够随着时间变化的数据,更多的...

    itclanCoder

扫码关注云+社区

领取腾讯云代金券