听风是风

学或不学,知识都在那里,只增不减。

导航

react 聊聊setState异步背后的原理,react如何感知setState下的同步与异步?

壹 ❀ 引

react中的setState是同步还是异步?react为什么要将其设计成异步?一文中,我们介绍了setState同步异步问题,解释了何种情况下同步与异步,异步带来了什么好处,以及react官方为何要将setState设计成异步。

但因为文章篇幅问题,我们遗留了一个与setState底层相关的问题,为什么在合成事件中使用setState会批量异步合并,而原生事件中setState又是同步呢?react是如何感知这两者的区分从而做不同处理,带着疑问文本开始。

贰 ❀ setState背后的秘密(旧版)

既然setState在合成与原生事件之间有所区分,那么在setState源码实现上一定会有所表现,这里我们摘出setState相关源码做一个简单分析。

注意,这里的源码版本为react 15,原因是我在阅读react 16源码过程中发现react在更新机制上已经有了Fiber的介入,若不了解Fiber理解起来就十分困难了:

enqueueSetState: function (inst, payload, callback) {
  var fiber = get(inst);
	// ....
  enqueueUpdate(fiber, update);
  scheduleWork(fiber, expirationTime);
},

所以还是先了解低版本的做法,对于后续理解Fiber也有一定帮助,先看看setState相关实现方法,这里做了部分代码裁剪:

ReactComponent.prototype.setState = function (partialState, callback) {
  // 本质上调用的是enqueueSetState这个方法
  this.updater.enqueueSetState(this, partialState)

  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState')
  }
}

当我们调用setState时,其实本质上调用的是this.updater.enqueueSetState,看到enqueue本能会想到队列,这里感觉就跟批量处理扯上关系了,OK,我们接着看enqueueSetState的实现:

enqueueSetState: function(publicInstance, partialState) {
  // 根据传递的this,获取当前组件实例
  var internalInstance = getInternalInstanceReadyForUpdate(
    publicInstance,
    'setState',
  );

  // 获取当前组件实例上的_pendingStateQueue
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);

  // 把当前要更新的状态push到数组中
  queue.push(partialState); 

  // 再次调用enqueue更新方法
  enqueueUpdate(internalInstance);
}

enqueueSetState做的事情也很简单,大致分为四步:

  1. 根据传递的this(参数publicInstance)获取当前组件的实例internalInstance
  2. 获取组件实例internalInstance上的数组_pendingStateQueue,看名字就知道是等待被处理的state状态,而且假如不存在,这里也会帮其初始化成一个数组。
  3. 将我们这一次要更新的state状态push到数组中。
  4. 调用队列更新方法enqueueUpdate

接着我们来看看enqueueUpdate相关实现:

var dirtyComponents = [];
function enqueueUpdate(component) {
  ensureInjected();

  // isBatchingUpdates决定了是否立刻更新this.state
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

这里,我们就看到大家有所耳闻的isBatchingUpdates(表示当前是否处理批量更新阶段)react会根据此字段来决定是否立刻更新状态。假设isBatchingUpdatesfalse,直接调用batchingStrategy.batchedUpdates做更新操作,假设为true,则将我们当前的组件实例加入dirtyComponents中,表示这个更新得再等一等。

那既然isBatchingUpdates是由batchingStrategy(批量更新策略)提供,我们接着看看它的内部实现:

var transaction = new ReactDefaultBatchingStrategyTransaction();
var ReactDefaultBatchingStrategy = {
  // 全局的isBatchingUpdates,一开始默认是false
  isBatchingUpdates: false, 
  batchedUpdates: function (callback, a, b, c, d, e) {
    // 这里是用于在修改isBatchingUpdates之前存储上次的isBatchingUpdates状态
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates

    // 只要调用batchedUpdates就会将isBatchingUpdates改为true
    ReactDefaultBatchingStrategy.isBatchingUpdates = true

    // 如果在我们改isBatchingUpdates为true之前它就已经是true了,那说明改之前就已经处于批量更新状态中了
    if (alreadyBatchingUpdates) {
      // 那既然已经在更新了,就直接等待更新结束
      return callback(a, b, c, d, e)
    } else {
      // 启动事务开始进行更新
      return transaction.perform(callback, null, a, b, c, d, e)
    }
  },
}

OK,到这里情况就有点复杂了,我们提炼下信息以及可能的疑问。

  1. ReactDefaultBatchingStrategy提供了全局的批量更新状态锁isBatchingUpdates,且一开始默认是false
  2. 假设我们调用batchedUpdates,会将isBatchingUpdates改为true
  3. 根据修改isBatchingUpdates之前的锁的状态决定不同的处理,锁是false直接等待更新结束,是true那就开始走事务更新。

问题来了,虽然ReactDefaultBatchingStrategy提供了isBatchingUpdates但这个东西一开始就是false啊,我们目前唯一看到的锁的修改还是在batchedUpdates中。但很明显在上面的enqueueUpdate中就已经先一步对于batchedUpdates状态做判断了,那说明一定有其它地方也会修改batchedUpdates的状态,否则同步异步的执行就完全没区别了。

我们可以尝试推理下异步与同步的差异过程,假设是异步情况,当走到enqueueUpdate时按照我们的理解,此时isBatchingUpdates就应该是true,这样代码才能走到dirtyComponents.push(component)这一步,让状态更新等一等,因此一定在更之前有什么操作将isBatchingUpdates改为true,这也逻辑才合理。

而同步情况参考一开始的isBatchingUpdates默认值是false,逻辑也确实也能走到立刻更新batchedUpdates,但是要注意,batchedUpdates中是会将isBatchingUpdates改为true的,那你在定时器中写了两个setState,第一次因为锁的默认值是false算你立刻更新了,但锁被改成true了第二次同步更新怎么办?按照常理来说,一定有一个更新完成后重置锁的状态为false的动作,不然这就说不通了。

PS:题外话说一句,不要在同一组件中同时使用同步与异步更新this.state,由上分的分析就能感受到,这种做法极大可能造成更新的混乱与不可预期。

叁 ❀ react中的Transaction(事务)

通过上面的分析,我们已经得知用于区分是否立刻更新还是等等再更新的关键在于批量更新锁batchedUpdates,但紧接着我们脑补了同步与异步的执行情况,推测一定有某个地方会做提前修改锁的状态,以及更新完成后重置锁状态类似的操作,那么在哪做的呢?谁来负责这一块的逻辑呢?这就得聊聊react中的事务处理Transaction

class Transaction {
  reinitializeTransaction() {
    // 获取wapper方法,是个数组
    this.transactionWrappers = this.getTransactionWrappers();
  }
  // 事务的启动方法
  perform(method, scope, ...param) {
    this.initializeAll(0);
    // 这里执行的method其实就是enqueueUpdate
    var ret = method.call(scope, ...param);
    this.closeAll(0);
    return ret;
  }
  // 执行所有wapper中的init
  initializeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.initialize.call(this);
    }
  }
  // 执行所有wapper中的close
  closeAll(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      wrapper.close.call(this);
    }
  }
}

class ReactDefaultBatchingStrategyTransaction extends Transaction {

  constructor() {
    this.reinitializeTransaction()
  }
  // 返回wapper方法,是个数组
  getTransactionWrappers() {
    return [
      // FLUSH_BATCHED_UPDATES
      {
        initialize: () => { },
        // state更新完后,diff对比以及组件后续更新
        close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
      },
      // RESET_BATCHED_UPDATES
      {
        initialize: () => { },
        close: () => {
          // 重置锁
          ReactDefaultBatchingStrategy.isBatchingUpdates = false;
        }
      }
    ]
  }
}

react的事务解释起来有点抽象,但我们可以先站在宏观的角度去理解,我们可以将Transaction理解成一个封闭的方法加工工厂,每当一个方法运输进去,都会通过wapper对方法进行加工,并为方法组装上initializeclose方法。

当调用transaction.perform启动事务处理时,你会发现在perform中的处理分为三步,第一执行所有wapper中的init;第二才是真正执行我们传递的callback(本质就是enqueueUpdate);第三步在callback跑完再执行所有wapper中的close

wapper其实也分为FLUSH_BATCHED_UPDATESRESET_BATCHED_UPDATES两种类型,不同类型中的init我们先不管,但FLUSH_BATCHED_UPDATES中的close会在callback执行完成后帮助我们更新最新的stateprops

当我们看向RESET_BATCHED_UPDATESclose时,我们发现了一个熟悉的操作ReactDefaultBatchingStrategy.isBatchingUpdates = false,这里是我们第二次发现修改锁的状态。还记得前面我们对于原生定时器中多次执行setState的问题吗?第一次setState会将isBatchingUpdates改为true,但在执行完完成后RESET_BATCHED_UPDATES中的close会帮我们立刻重置锁的状态,这也就保证了定时器中第二个setState运行时,锁的状态又默认成了false,于是再次同步更新。

肆 ❀ 为什么钩子合成事件是异步?

事务介绍了一大堆,我们顺利解释了同步更新情况下setState是如何重置锁状态的,那么钩子函数执行setState得保证锁一开始就是true才行啊,这又是怎么回事呢?看下面这段代码:

// 摘自上方的batchedUpdates方法,知道里面有将锁改为true的操作就行
batchedUpdates: function (callback, a, b, c, d, e) {
// ...
  ReactDefaultBatchingStrategy.isBatchingUpdates = true
  if (alreadyBatchingUpdates) {
    return callback(a, b, c, d, e)
  } else {
    return transaction.perform(callback, null, a, b, c, d, e)
  }
}

_renderNewRootComponent: function( nextElement, container, shouldReuseMarkup, context ) {
  var componentInstance = instantiateReactComponent(nextElement);
  // 调用batchedUpdates方法
  ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,
    componentInstance,
    container,
    shouldReuseMarkup,
    context
  );
}

_renderNewRootComponent其实看名字就知道是在组件渲染时执行的方法,也就是在组件初次渲染,这里就已经执行过一次batchedUpdates方法了,而batchedUpdates内部有将锁改为true的操作,这也就是为啥钩子函数中setState异步的问题。

同理,合成事件中的setState也是异步,那说明也应该有初始锁状态为true的行为,事实上确实如此,看下面代码:

dispatchEvent: function (topLevelType, nativeEvent) {
  try {
    // 调用了batchedUpdates修改锁状态
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  } finally {
    TopLevelCallbackBookKeeping.release(bookKeeping);
  }
}

那么到这里,我们解释了钩子函数,合成事件以及原生事件中setState执行差异背后的原理。

伍 ❀ 总

其实本文一开始我是打算通过setState的差异性引出合成事件,从而介绍合成事件与原生事件的区别。但在整理setState的过程中,发现信息量惊人....而且更为离谱的是,react16.8引入fiber开始,setState原理其实已经不再是上述那样了。但处于篇幅问题以及知识量,此篇仅介绍react 15同异步原理差异,那么下一篇正式介绍react中的合成事件,本文结束。

参考

深入 React 的 setState 机制

React 架构的演变 - 从同步到异步

react15 和 react16 在 setState 后的更新渲染解析

posted on 2021-11-13 20:59  听风是风  阅读(502)  评论(0编辑  收藏  举报