浅入深出setState(下篇)

上篇: https://segmentfault.com/a/11...

Part one - 同步 or 异步

1. 先说现象吧:

在React中,若是是由React引起的事件处理(好比经过onClick引起的合成事件处理)和组件生命周期函数内(好比componentDidMount),调用this.setState不会同步更新this.state,除此以外的setState调用会同步执行this.state。
所谓“除此以外”,指的是绕过React经过addEventListener直接添加的事件处理函数,还有经过setTimeout/setInterval产生的异步调用。react

若是咱们按照教科书般的方式来使用React,基本上不会触及所谓的“除此以外”状况。

2. 再说为何会这样:(其实只要解开了这个疑问,才能明白setState的异步队列是如何实现的)

在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state仍是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,可是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改成true,而当React在调用事件处理函数和自身生命周期以前就会调用这个batchedUpdates,形成的后果,就是由React控制的事件处理过程和生命周期中的同步代码调用setState不会同步更新this.statewebpack

注意:同步代码调用,在合成事件和生命周期内的异步调用setState(好比ajax和setTimeout内),也是会同步更新this.setState。

Demo请看上篇的Part Fourgit

因此按照正常React用法都是会通过batchingUpdate方法的。这是因为React有一套自定义的事件系统和生命周期流程控制,使用原生事件监听和settimeout这种方式会跳出React这个体系,因此会直接更新this.state。github

咱们在看代码是如何实现的,须要了解这样一个东西“事务”,React内部的工具方法实现了一个可供使用的事务。web

Part two - React中的事务

React中的事务借用了计算机专业术语的单词Transaction 。对比数据库的事务性质,二者之间有共同点却又不是一回事,简答来讲 把须要执行的方法用一个容器封装起来,在容器内执行方法的先后,分别执行init方法和close方,其次来讲,一个容器能够包裹另外一个容器,这点又相似于洋葱模型。ajax

React的合成事件系统和生命周期就使用了React内部实现的事务,为其函数附加了先后两个相似npm脚本pre和post两个钩子的事件。数据库

这是一个npm srcipt的例子:npm

"prebuild": "echo I run before the build script",
"build": "cross-env NODE_ENV=production webpack",
"postbuild": "echo I run after the build script"

//用户执行npm run build就会实际执行
npm run prebuild && npm run build && npm run postbuild

//所以能够在两个钩子里作一些准备工做和清理工做。

有过有兴趣,咱们来看一下如何简单使用事务:https://codesandbox.io/s/6xl5yrjvzzsegmentfault

var MyTransaction = function () {
  //...
};

Object.assign(MyTransaction.prototype, Transaction.Mixin, {
  getTransactionWrappers: function () {
    return [
      {
        initialize: function () {
          console.log("before method perform");
        },
        close: function () {
          console.log("after method perform");
        }
      }
    ];
  }
});

var transaction = new MyTransaction();

transaction.reinitializeTransaction()

var testMethod = function () {
  console.log("test");
};

transaction.perform(testMethod);

Part three - 哪有什么岁月静好,不过是有人为你负重前行

因此,咱们能够获得启发,React的事件系统和生命周期事务先后的钩子对isBatchingUpdates作了修改,其实就是在事务的前置pre内调用了batchedUpdates方法修改了变量为true,而后在后置钩子又置为false,而后发起真正的更新检测,而事务中异步方法运行时候,因为JavaScript的异步机制,异步方法(setTimeout等)其中的setState运行时候,同步的代码已经走完,后置钩子已经把isBatchingUpdates设为false,因此此时的setState会直接进入非批量更新模式,表如今咱们看来成为了同步SetState。数组

尝试在描述一下:整个React的每一个生命周期和合成事件都处在一个大的事务当中。原生绑定事件和setTimeout异步的函数没有进入React的事务当中,或者是当他们执行时,刚刚的事务已经结束了,后置钩子触发了,close了。(你们能够想想分别是哪种状况)。

React“坐”在顶部调用堆栈框架并知道全部React事件处理程序什么时候运行,setState在React管理的合成事件或者生命周期中调用,它会启用批量更新事务,进入了批量更新模式,全部的setState的改变都会暂存到一个队列,延迟到事务结束再合并更新。若是setState在React的批量更新事务外部或者以后调用,则会当即刷新。

懂得了事务,再回看,就明白,其实setState历来都是同步运行,不过是React利用事务工具方法模拟了setState异步的假象。

延迟队列如何实现,其实有悟性的同窗已经能够大概猜到。咱们再来捋一捋,看源码是否能验证咱们的结论描述和现象。

Part four - 源码验证

能够对照这张图先来看下setState的流程代码,代码仓库在个人github的react-source仓库,目录已被我精简,剩下关键的源代码文件夹。

图片描述

首先,咱们搜索setState =看下setState何处被赋值,找到了这里

// src/isomorphic/modern/class/ReactComponent.js
/*
 * React组件继承自React.Component,而setState是React.Component的方法,
 * 所以对于组件来说setState属于其原型方法,首先看setState的定义:   
 */
ReactComponent.prototype.setState = function(partialState, callback) {
    // 忽略掉入参验证和开发抛错
   //调用setState实际是调用了enqueueSetState
   // 调用队列的入队方法,把当前组件的示例和state存进入
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    // 若是有回调,把回调存进setState队列的后置钩子
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

会发现调用setState实际是调用this.updater.enqueueSetState,此时咱们不得不看一看updater及其enqueueSetState方法是什么东西,咱们在当前文件搜索:

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  // updater有默认值,真实运行时会注入,其实也算依赖注入
  this.updater = updater || ReactNoopUpdateQueue;
}

ReactNoopUpdateQueue是一个这样的对象,提供了基本的无效方法,真正的updater只有在React被真正加载前才会被注入进来,运行时注入,严格来讲是依赖注入,是React源码的风格之一。

// src/isomorphic/modern/class/ReactNoopUpdateQueue.js
var ReactNoopUpdateQueue={
    isMounted: function(publicInstance) {
    return false;
  },
  enqueueCallback: function(publicInstance, callback) { },
  enqueueForceUpdate: function(publicInstance) { },
  enqueueReplaceState: function(publicInstance, completeState) { },
  enqueueSetState: function(publicInstance, partialState) { },
}

真实的enqueueSetState在这个文件内,方法把将要修改的state存入组件实例的internalInstance数组中,这里就是state的延迟更新队列了。而后立马调用了一个全局的ReactUpdates.enqueueUpdate(internalInstance)方法。

// src/renderers/shared/reconciler/ReactUpdateQueue.js

  // 这个是setState真正调用的函数
  enqueueSetState: function(publicInstance, partialState) {
       // 忽略基本的容错和抛错
    // 存入组件实例,准备更新
    var internalInstance = publicInstance;
    // 更新队列合并操做 更新 internalInstance._pendingStateQueue
    var queue = internalInstance._pendingStateQueue ||(internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
    // 发生了什么?猜一下?   ReactUpdates.js
  },
题外话:每一个函数的健壮入参判断和运行环境判断和完善的抛错机制,对来源不信任甚至是内部互调,是React对开发者友好的一个重要缘由。

咱们来猜下ReactUpdates.enqueueUpdate干了什么?根据上面的流程图我猜测应当是判断流程。

function enqueueUpdate(component) {
  ensureInjected(); //环境判断:是否有调度事务方法同时有批量更新策略方法
  //关键的判断条件,是不是批量更新
  //但是isBatchingUpdates这个值谁来维护呢?
  if (!batchingStrategy.isBatchingUpdates) {        // 见词知意 若是不在批量更新策略中
    // 若是不是批量更新,猜测一下,应该会当即更新吧?
    // 唉?batchingStrategy到底在作什么呢
    batchingStrategy.batchedUpdates(enqueueUpdate, component);  // 调用事务
    // 对队列中的更新执行 batchedUpdates 方法
    return;
  }
  // 若是是批量更新,那就把组件放入脏组件队列,也就是待更新组件队列
  dirtyComponents.push(component);
}

须要看ReactDefaultBatchingStrategy.js 看 batchedUpdates 方法,这个js文件就有意思了,一上来就是咱们以前提到的事务。

避免枯燥,我用人话阐述一下这个js的内容,也能够直接看ReactDefaultBatchingStrategy.js

var ReactDefaultBatchingStrategy={
    isBatchingUpdates:false,
    batchedUpdates:function(){},
}

文件底部声明了 ReactDefaultBatchingStrategy对象,内部isBatchingUpdates初始值为false,这个就是咱们心心念念判断是否在批量更新策略的重要变量。
这个isBatchingUpdates变量搜索整个项目,发现它只被两处改变:

  1. 对象自身的另外一个batchedUpdates方法固定赋值为true,标识着开启批量更新策略。
  2. 一个事务的close钩子,设为false,标识着结束批量更新策略。刚好,这个事务被batchedUpdates调用。

实质上,isBatchingUpdates仅仅也就是被batchedUpdates方法维护着,batchedUpdates调用时开启批量更新,同时入参callback被事务包裹调用,callback调用完成时候事务close钩子触发,关闭批量更新模式。事务的close钩子函数有两个,另外一个以前会调用ReactUpdates.flushBatchedUpdates方法,也就是真正的把积攒的setState队列进行更新计算。

问题来了,callback是啥,batchedUpdates方法在setState以前,或者说除了setState还会被谁调用,致使isBatchingUpdates变为true,我猜测是生命周期函数和合成事件,只有这样,整个维护批量更新策略的机制就造成了闭环,验证了咱们以前的结论。

咱们搜索batchedUpdates(,果不其然,在src/renderers/dom/client/ReactEventListener.jssrc/renderers/dom/client/ReactMount.js中找到了ReactUpdates.batchedUpdates的调用。

合成事件和生命周期的装载发生时,调用了batchedUpdates方法,使得内部的同步代码均可以运行在批量更新策略的事务环境中,结束后,便使用事务的后置钩子启动merge更新,重置常量。

另外我在ReactDOM.js发现了对React顶层API对batchedUpdates方法的引用,可让 Promise 这些异步也能进入 batch update:

unstable_batchedUpdates: ReactUpdates.batchedUpdates,

另外一个彩蛋,虽然React不提倡使用这个API,之后版本也可能移除,可是如今咱们能够这样在React中这样使用:

React.unstable_batchedUpdates(function(){
    this.setState({...})
    this.setState({...})
    //...在此函数内也可使用批量更新策略
})

解决了setTimeout和AJAX异步方法、原生事件内的setState批量更新策略失效的问题,让批量更新在任何场景都会发生。

Part five - 总结

图片描述

  1. this.setState首先会把state推入pendingState队列中。
  2. 而后将组建标记为dirtyComponent。
  3. React中有事务的概念,最多见的就是更新事务,若是不在事务中,则会开启一次新的更新事务,更新事务执行的操做就是把组件标记为dirty。
  4. 判断是否处于batch update。
  5. 是的话,保存组建于dirtyComponent中,在事务的时候才会经过 ReactUpdates.flushBatchedUpdates 方法将全部的临时 state merge 并计算出最新的 props 及 state,而后将其批量执行,最后再关闭结束事务。
  6. 不是的话,直接开启一次新的更新事务,在标记为dirty以后,直接开始更新组件。所以当setState执行完毕后,组件就更新完毕了,因此会形成定时器同步更新的状况。
相关文章
相关标签/搜索