React学习(一)setState的更新机制

1、Transation

      在上一篇文章中讲到在调用ReactDOM.render方法渲染组件时,其主要功能是通过ReactMount 文件下的_renderSubtreeIntoContainer方法实现的,该方法主要将组件渲染分为三个步骤:
(1) Diff算法判断新的虚拟DOM差异,首次渲染可以跳过
(2) 将虚拟DOM实例化
(3) 将实例化后的DOM写入到container中
步骤(3)调用了ReactUpdates.batchedUpdates方法,它的第一个参数是batchedMountComponentIntoNode方法,来看一看这个方法的源码

  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
  /* useCreateElement */
  !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement);
  transaction.perform(mountComponentIntoNode, null, componentInstance, container, transaction, shouldReuseMarkup, context);
  ReactUpdates.ReactReconcileTransaction.release(transaction);
}

可见,ReactUpdates是通过调用ReactUpdates.ReactReconcileTransaction 的transaction.perform()实现的,为什么调用perform呢?何为transation?
官方解释如下:

Transactioncreates a black box that is able to wrap any method such that certain invariants are maintained before and after the method is invoked (Even if an exception is thrown while invoking the wrapped method). Whoever instantiates a transaction can provide enforcers of the invariants at creation time. TheTransactionclass itself will supply one additional automatic invariant for you - the invariant that any transaction instance should not be run while it is already being run. You would typically create a single instance of aTransaction` for reuse multiple times, that potentially is used to wrap several different methods. Wrappers are extremely simple - they only require implementing two methods.

Transaction对需要执行的方法进行封装,只允许你在当前没有其他事物被运行时才运行当前事物。其结构如下:


image.png

   Transaction将需要执行的函数封装成两个wrapper,每个wrapper包含了initialize方法和close方法。执行一个transaction其实就是调用它的perform,源码如下:

    /* eslint-enable space-before-function-paren */
    var errorThrown;
    var ret;
    try {//标志当前处于事物正在执行 ,将
      this._isInTransaction = true;
      errorThrown = true;
     //事物排队
      this.initializeAll(0); 
     // 执行method
      ret = method.call(scope, a, b, c, d, e, f);
      errorThrown = false;
    } finally {
// 执行结束后 close transaction。
      try {
        if (errorThrown) {
          try {
            this.closeAll(0);
          } catch (err) {}
        } else {
          this.closeAll(0);
        }
      } finally {
       //将this._isInTransaction设置为false,结束当前事物,标志其他transform可以执行。
        this._isInTransaction = false;
      }
    }
    return ret;
  }

可见,transaction的perform方法其实就是对call方法进行了封装。
      在执行transaction时,首先会先调用initializeAll()进行将需要进行的操作加入临时队列,

initializeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
        this.wrapperInitData[i] = OBSERVED_ERROR;
        this.wrapperInitData[i] = wrapper.initialize ? wrapper.initialize.call(this) : null;
      } finally {
        if (this.wrapperInitData[i] === OBSERVED_ERROR) {
        try {
            this.initializeAll(i + 1);
          } catch (err) {}
        }
      }
    }
  }

当transaction执行结束时会调用close结束当前事物。

closeAll: function (startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var initData = this.wrapperInitData[i];
      var errorThrown;
      try {
        errorThrown = true;
        if (initData !== OBSERVED_ERROR && wrapper.close) {
          wrapper.close.call(this, initData);
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          try {
            this.closeAll(i + 1);
          } catch (e) {}
        }
      }
    }
    this.wrapperInitData.length = 0;
  }

可见,batchUpdate 功能都是通过执行各种 transaction 实现的。当虚拟DOM实例化之后并没有立刻插入到DOM中,而是通过 ReactUpdates.batchedUpdate 方法存入临时队列中。当一个 transaction 完成后,才会context写到container中。
batchedMountComponentIntoNode(componentInstance, container, shouldReuseMarkup, context)
在React中还有很多地方使用到了transaction,比如this.setState()。就是我们今天的主题:

2、React的更新机制

      React更新机制来源于一个React.js网站React Kung Fu
,在此安利一下,个人觉得对学习react很有帮助。
      在react需要更新时,通常需要调用setState(),我们来看一个实例:

var Counter = React.createClass({
  getInitialState: function () {
    return { clickCount: 0 };
  },
  handleClick: function () {
    this.setState(function(state) {
      return {clickCount: state.clickCount + 1};
    });
  },
  render: function () {
    return (<h2 onClick={this.handleClick}>点我!点击次数为: {this.state.clickCount}</h2>);
  }
});
ReactDOM.render(
  <Counter />,
  document.getElementById('message')
);

在React文件中,其组件来自ReactBaseClasses文件下的ReactComponent,setState是它的一个方法。我们来看一看在ReactBaseClasses文件下的源码:

function ReactComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}
ReactComponent.prototype.setState = function (partialState, callback) {
   this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

      setState方法主要做了两件事情:
一是将setState放入updater的SetState 队列;
二是将callback放入updater的Callback队列。
      在setState()方法中,使用了this.updater对象,那么什么是updater呢?顾名思义,它是一个更新作用的对象,定义在ReactClass 和 ReactComponent中,定义如下:

this.updater = updater || ReactNoopUpdateQueue; 

      如果没有传入参数updater,那么this.updater的值就是ReactNoopUpdateQueue来进行初始化。而ReactNoopUpdateQueue.enqueueSetState主要起到一个在非生产版本中警告(warning)的作用。真正的updater是在render中注入(inject)的。因此如果你在constructor中尝试调用setState,也会给出相应的警告表明在非安装或已卸载的组件中不能使用setState。
2-1 updater
      那么updater是如何注入的呢?在React Kung Fu网有这么一句话:

React.js codebase relies heavily on a dependency injection principle. This allows to substitute parts of React.js based on the environment (server-side vs. client-side, different platforms) in which you’re rendering. ReactComponent is a part of the isomorphic namespace - it will always exist, no matter it is React Native, ReactDOM on browser or server-side. Also it contains only pure JavaScript which should run on every device capable of understanding the ECMAScript 5 standard of JS.

      React.js的源码大量地依赖于注入原则,实现在其他平台环境的渲染。ReactComponent脚本存在于isomorphic目录这意味着它支持异构,即它可用于React Native,在浏览器端或服务器端运行的ReactDOM。那么真实的updater在哪里注入的呢?

* 初始化组件, 渲染层和注册事件监听器。
* @param {ReactReconcileTransaction|ReactServerRenderingTransaction} transaction
* @param {?object} hostParent
* @param {?object} hostContainerInfo
   * @param {?object} context
   * @return {?string} Rendered markup to be inserted into the DOM.
   * @final
   * @internal
   */
  mountComponent: function (transaction, hostParent, hostContainerInfo, context) {
…
var updateQueue = transaction.getUpdateQueue();
var inst = this._constructComponent(doConstruct, publicProps, publicContext, updateQueue);
inst.props = publicProps;
    inst.context = publicContext;
    inst.refs = emptyObject;
    inst.updater = updateQueue;
}
_constructComponent方法的返回值是同文件下_constructComponentWithoutOwner的返回值:
_constructComponent: function (doConstruct, publicProps, publicContext, updateQueue){
…
return this._constructComponentWithoutOwner(doConstruct, publicProps, publicContext, updateQueue);
}
   functio_constructComponentWithoutOwner:function (doConstruct, publicProps, publicContext, updateQueue) {
    …
          return new Component(publicProps, publicContext, updateQueue);
    …
    }

      由此可见,更新队列updateQueue是在_constructComponentWithoutOwner方法中注入。现在知道了何为updater,接下来我们回归到setState中的两个回调方法enqueueSetState和enqueueCallback。
未完待续。。。
继续。。。
2-2 enqueueSetState和enqueueCallback
      在ReactUpdateQueue.js中找到这两个方法的源码:

enqueueSetState: function (publicInstance, partialState) {
   … …
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
    if (!internalInstance) {
      return;
    }
    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);
    enqueueUpdate(internalInstance);
  },
enqueueCallback: function (publicInstance, callback, callerName) {
    ReactUpdateQueue.validateCallback(callback, callerName);
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
    if (!internalInstance) {
      return null;
    }
    if (internalInstance._pendingCallbacks) {
      internalInstance._pendingCallbacks.push(callback);
    } else {
      internalInstance._pendingCallbacks = [callback];
    }
    enqueueUpdate(internalInstance);
  },

      从上面两个函数可以发现,他们都使用了enqueueUpdate函数,这两个函数的逻辑如下:
      (1) 创建对象internalInstance,它是getInternalInstanceReadyForUpdate的实例对象。

function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
  var internalInstance = ReactInstanceMap.get(publicInstance);
}

      由此可见internalInstance其实是ReactInstanceMap的实例,getInternalInstanceReadyForUpdate只是起到委托的作用。而ReactInstanceMap 是一个操作实例对象的函数封装。注:state初始化时会调用ReactInstanceMap.set方法

set: function (key, value) {
    key._reactInternalInstance = value;
  }

在更新队列时,用get方法取其值。初次之外还有remove和has方法。
      (2) 对internalInstance进行修改,将setState写入internalInstance._pendingStateQueue队列中,将callback写入_pendingCallbacks。
      (3) 最后调用enqueueUpdate(internalInstance)刷新更新。
来看一看enqueueUpate是如何实现刷新更新的:

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

因此,由于ReactUpdate是中有共享方法,而它得问依赖是被注入的。
      enqueueUpate是如何通过引用ReactUpdates.enqueueUpdate方法实现flush更新的。在ReactUpdates.js下找到enqueue的源码有:

/**
 * Mark a component as needing a rerender, adding an optional callback to a
 * list of functions which will be executed once the rerender occurs.
 */
function enqueueUpdate(component) {
  ensureInjected();
  // Various parts of our code (such as ReactCompositeComponent's
  // _renderValidatedComponent) assume that calls to render aren't nested;
  // verify that that's the case. (This is called by each top-level update
  // function, like setState, forceUpdate, etc.; creation and
  // destruction of top-level components is guarded in ReactMount.)
// 如果当前没有分批处理操作则使用batchingStrategy.batchedUpdates分批处理更新队列,结束后返回
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
// 如果当前有分批处理操作,则把需要更新的组件加入dirtyComponents队列中

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
enqueueUpdate的功能实现由两个重要的步骤,分别是ensureInjected()和
batchingStrategy(一种批量处理更新机制的策略)。
ensureInjected的源码如下:
function ensureInjected() {
  !(ReactUpdates.ReactReconcileTransaction && batchingStrategy) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'ReactUpdates: must inject a reconcile transaction class and batching strategy') : _prodInvariant('123') : void 0;
}

      由ensureInjected的代码逻辑可知,ReactUpdate必须要注入ReactUpdates.ReactReconcileTransaction(一个解调的事物类)和batchingStrategy(批量处理策略)。BatchingStrategy是一种React批量处理更新的策略。在源码中,当且仅有一个策略是ReactDefaultBatchingStrategy。ReactReconcileTransaction 依赖于环境,负责处理更新后的事物状态,比如在DOM中,修复更新后导致文本选择状态的丢失问题、在解调期间禁止事件和生命周期的方法进入队列。
      enqueueUpdate的有些难理解,特别是第一眼好像并没有发生特别的事情。BatchingStrategy 能告诉你当前是否有transaction处于进程中。如果不在,enqueueUpdate会停下来,将它自身注册到transaction中并执行。然后一个组件会被添加到dirty组件列表中。但是到目前为止,还没有搞清楚状态是什么致使状态更新的。为了理解这个过程是如何发生的,我们必须要搞清楚batchingStrategy是从哪里注入的,传入了什么参数。
      我们从ReactDOM入口文件开始找inject,在该文件require文件下的第一行,有ReactDefaultInjection.inject();找到ReactDefaultInjection文件,在它的更新属性中有

ReactInjection.Updates.injectReconcileTransaction(ReactReconcileTransaction);
 ReactInjection.Updates.injectBatchingStrategy(ReactDefaultBatchingStrategy);

找到ReactInjection,我们来看一看它是定义update:

var ReactInjection = {
  Updates: ReactUpdates.injection
};

继续向下ReactUpdates.js文件

var ReactUpdates = {
    injection: ReactUpdatesInjection,
};
var ReactUpdatesInjection = {
injectBatchingStrategy: function (_batchingStrategy) {
        batchingStrategy = _batchingStrategy;
  }
  }

      到此为止,我们知道ReactInjection.Updates = ReactUpdatesInjection .injectReconcileTransaction;现在来看传入到其中的参数_batchingStrategy为何物,也就是ReactDefaultInjection文件中的传入的参数ReactDefaultBatchingStrategy。ReactDefaultBatchingStrategy中有两个封装对象RESET_BATCHED_UPDATES 和FLUSH_BATCHED_UPDATES 。
      (1) RESET_BATCHED_UPDATES:负责在事物结束后清理isBatchingUpdates的标志位;
      (2) FLUSH_BATCHED_UPDATES:

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

      会在事物结束后调用来自ReactUpdates一个方法flushBatchedUpdates,它是状态更新的核心代码。我们来看一看了flushBatchedUpdates是如何实现状态更新的,在ReactUpdates文件下找到flushBatchedUpdates的定义

var flushBatchedUpdates = function () {
  // ReactUpdatesFlushTransaction's wrappers will clear the dirtyComponents
  // array and perform any updates enqueued by mount-ready handlers (i.e.,
  // componentDidUpdate) but we need to check here too in order to catch
  // updates enqueued by setState callbacks and asap calls.
  while (dirtyComponents.length || asapEnqueued) {
    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }
    if (asapEnqueued) {
      asapEnqueued = false;
      var queue = asapCallbackQueue;
      asapCallbackQueue = CallbackQueue.getPooled();
      queue.notifyAll();
      CallbackQueue.release(queue);
    }
  }
};

      在此有出现了一个新的事物ReactUpdatesFlushTransaction,它主要用来捕获在运行flushBatchedUpdate后将要运行的updates。这个过程比较复杂,因为componentDidUpdate或则setState后的回调方法需要进入下一个更新队列。另外这个事物是getpooled来的,而不是实时创建的,这样做的好处是避免不必要的垃圾收集。另外这个地方也涉及到asp update的内容,后续将介绍到。
未完待续。。。

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

推荐阅读更多精彩内容