阅读 1583

React setState源码阅读

本文基于React15总结,最新React16可能有一些出入,望周知!!!

setState真的是异步的吗 ?

举个🌰

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

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};
复制代码

问上述代码中 4 次 console.log 打印出来的 val 分别是多少? 不卖关子,先揭晓答案,4 次 log 的值分别是:0、0、2、3。 若结果和你心中的答案不完全相同,那下面的内容你可能会感兴趣。 同样的 setState 调用,为何表现和结果却大相径庭呢?让我们先看看 setState 到底干了什么。

先放结论:

  • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout 中都是同步的。
  • setState 的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  • setState的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次setState,setState的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState多个不同的值,在更新时会对其进行合并批量更新。

所以基于上述结论,如果想要实现上述代码中 4 次 console.log 打印出来的 val 分别是1、2、3、4。可以实现如下:

setTimeout(() => {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 1

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 2

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 3

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);  // 4
}, 0);
复制代码

或者

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 1

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 2

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 3

this.setState((prevState) => {
    return { count: prevState.val + 1 }
})
console.log(this.state.val);  // 4
复制代码

setState 干了什么

image

上面这个流程图是一个简化的 setState 调用栈,setState 方法由父类 Component 提供(因为组件本身继承自React.Component),是 React 组件修改局部状态的方法。

// src/isomorphic/modern/class/ReactComponent.js
ReactComponent.prototype.setState = function(partialState, callback) {
  // ...
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback);
  }
};
复制代码
// src/renderers/shared/reconciler/ReactUpdateQueue.js
enqueueSetState: function(publicInstance, partialState) {
  // 获取 ReactComponent 组件对象(这里的组件对象指的是调用了this.setState的组件)
  var internalInstance = getInternalInstanceReadyForUpdate(
    publicInstance,
    'setState'
  );

  if (!internalInstance) {
    return;
  }
  // 将 partialState 放入组件的状态队列
  var queue =
    internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState);

  enqueueUpdate(internalInstance);
},
复制代码

上述流程图中核心的状态判断,在源码(ReactUpdates.js)

function enqueueUpdate(component) {
 // ...
​
// 如果不是正处于创建或更新组件阶段,则处理 update 事务
 if (!batchingStrategy.isBatchingUpdates) {
   batchingStrategy.batchedUpdates(enqueueUpdate, component);
   return;
 }
​
// 如果正在创建或更新组件,暂且先不处理 update,只是将组件放在 dirtyComponents 数组中
  dirtyComponents.push(component);
}
复制代码

在执行setState的时候,React Component将newState存入了自己的等待队列,然后使用全局的批量策略对象batchingStrategy来查看当前执行流是否处在批量更新中,如果已经处于更新流中,就将记录了newState的React Component存入dirtyeComponent中,如果没有处于更新中,遍历dirty中的component,调用updateComponent,进行state或props的更新,刷新component。

那么batchingStrategy究竟是何方神圣呢?其实它只是一个简单的对象,定义了一个 isBatchingUpdates 的布尔值,和一个 batchedUpdates 方法。下面是一段简化的定义代码:

var batchingStrategy = {
 isBatchingUpdates: false,
​
 // 这里的 callback 其实就是上文中的enqueueUpdate函数。
 batchedUpdates: function(callback, a, b, c, d, e) {
   // 批处理最开始时,将 isBatchingUpdates 设为 true,表明正在更新
   batchingStrategy.isBatchingUpdates = true;

   transaction.perform(callback, null, a, b, c, d, e);
 }
};

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    // 事务批更新处理结束时,将isBatchingUpdates设为了false
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};

// 真正遍历 dirtyComponents 执行更新任务是在这个 wrapper 的 close 函数里
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};

// 批量更新事务的 wrappers
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
复制代码

第一个component进入到enqueueUpdate函数时,全局对象batchingStrategy的属性isBatchingUpdates默认是false,所以会直接执行batchingStrategy.batchedUpdates(enqueueUpdate, component);将全局对象batchingStrategy的属性isBatchingUpdates赋值true。然后执行transaction.perform(callback, null, a, b, c, d, e);。这里的callback也就是上文中的enqueueUpdate,callback 会在事务流程中执行。在事务中执行callback的时候就会把第一个component放入dirtyComponents中,因为此时isBatchingUpdates已经是true。

从下文事务执行流程(先执行所有 wrapper 中的 initialize 方法;然后执行perform;最后再执行所有的 close 方法)可知,RESET_BATCHED_UPDATES对象(close方法)负责在事务批更新处理结束时,将isBatchingUpdates设为了false,标识一次批处理更新结束。所以可知:RESET_BATCHED_UPDATES主要用来管理isBatchingUpdates状态

初识 Transaction

熟悉 MySQL 的同学看到 Transaction 是否会心一笑?然而在 React 中 Transaction 的原理和行为和 MySQL 中并不完全相同,让我们从源码开始一步步开始了解。

在 Transaction 的源码中有一幅特别的 ASCII 图,形象的解释了 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 封装起来,再通过 Transaction 提供的 perform 方法执行。而在 perform 之前,先执行所有 wrapper 中的 initialize 方法然后执行perform最后(即 method 执行后)再执行所有的 close 方法一组 initialize 及 close 方法称为一个 wrapper,从上面的示例图中可以看出 Transaction 支持多个 wrapper 叠加。

具体到实现上,React 中的 Transaction 提供了一个 Mixin 方便其它模块实现自己需要的事务。而要使用 Transaction 的模块,除了需要把 Transaction 的 Mixin 混入自己的事务实现中外,还需要额外实现一个抽象的 getTransactionWrappers 接口。这个接口是 Transaction 用来获取所有需要封装的前置方法(initialize)和收尾方法(close)的,因此它需要返回一个数组的对象,每个对象分别有 key 为 initialize 和 close 的方法。

下面是一个简单使用 Transaction 的例子

var Transaction = require('./Transaction');

// 我们自己定义的 Transaction
var MyTransaction = function() {
  // do sth.
};

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();
var testMethod = function() {
  console.log('test');
}
transaction.perform(testMethod);

// before method perform
// test
// after method perform
复制代码

当然在实际代码中 React 还做了异常处理等工作,这里不详细展开。有兴趣的同学可以参考源码中 Transaction 实现。

说了这么多 Transaction,关于上文提到的RESET_BATCHED_UPDATES主要用来管理isBatchingUpdates状态这句话是不是;理解更透彻了呐?

上文提到了两个wrapper:RESET_BATCHED_UPDATES和FLUSH_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会在一个transaction的close阶段运行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数组,主要干两件事。首先执行performUpdateIfNecessary来刷新组件的view,然后执行之前阻塞的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);
  }
},
复制代码

最后惊喜的看到了receiveComponent和updateComponent吧。receiveComponent最后会调用updateComponent,而updateComponent中会执行React组件存在期的生命周期方法,如componentWillReceiveProps, shouldComponentUpdate, componentWillUpdate,render, componentDidUpdate。 从而完成组件更新的整套流程。

总结

setState流程还是很复杂的,设计也很精巧,避免了重复无谓的刷新组件。它的主要流程如下:

1. enqueueSetState将state放入队列中,并调用enqueueUpdate处理要更新的Component;

2.如果组件当前正处于update事务中,则先将Component存入dirtyComponent中。否则调用batchedUpdates处理。

3.batchedUpdates发起一次transaction.perform()事务;

4.开始执行事务初始化,运行,结束三个阶段;

  • 初始化:事务初始化阶段没有注册方法,故无方法要执行;
  • 运行:执行setSate时传入的callback方法,一般不会传callback参数;
  • 结束:更新isBatchingUpdates为false,并执行FLUSH_BATCHED_UPDATES这个wrapper中的close方法。

5.FLUSH_BATCHED_UPDATES在close阶段,会循环遍历所有的dirtyComponents,调用updateComponent刷新组件,并执行它的pendingCallbacks, 也就是setState中设置的callback。

参考原文:

  1. React 源码剖析系列 - 解密 setState
  2. setState 之后发生了什么 —— 浅谈 React 中的 Transaction
  3. setState 源码分析,流程详解
文章分类
前端
文章标签