深入react的setState机制

5,501 阅读18分钟

本文研究的reat源码是v0.8.0版本的。从这个版本开始,沿着版本更迭的轨迹,我们可以看清setState机制的进化史。

setState进化史简介

关于react的setState机制,我相信有两个术语是人尽皆知的,那就是“批量更新”和“异步执行”。其实这两个术语都是在描述同一件事。为什么这么说呢?因为深入过源码的人就知道,“批量更新”是因,“异步执行”是果。世人都喜欢看表象和结果,所以一般情况下,我就用“异步执行”或者“同步执行”来进行相关的阐述。

使用react来开发的时间有点年头了,我依稀记得在很早的版本,一开始setState的执行是同步的,后面又改为在某种场景下是异步执行的,某种场景下是同步执行。最后到了现在,是统统异步执行。补充一下:经过查阅change log,react是在v0.13.0这个版本把setState统统改为异步执行的。有文为证:

Calls to setState in life-cycle methods are now always batched and therefore asynchronous. Previously the first call on the first mount was synchronous.

在v0.8.0版本中,在event listener中的setState是异步执行的,而在first call on the first mount中,也就是componentDidMount生命周期函数里面是同步执行的)。不信?咱们使用reactv0.8.0来验证以下:

import React from 'react';

const Count  = React.createClass({
  getInitialState() {
        return {
            count: 0
        }
   },

  render() {
    return <button onClick={()=> {
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      }}>{this.state.count}</button>
  }

  componentDidMount() {
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
  }
})

export default Count; 

初始挂载后,在componentDidMount里面的log结果是这样的:

1
2
3

此时,按钮上的数字为3。这证明了在componentDidMount这个生命周期函数里面,setState的执行是同步的。

如果我们接着click一下按钮,按钮的数字仅仅在原基础上增加1(而不是增加3),变为4。再看看log的结果是:

3
3
3

可想而知,在click的event listener里面setState的调用是异步的,因此,我们不能在紧接着去获取到最新的state值。如果我们想获取到最新的state值的话:

  • 要么在setState方法的callback里面获取;
  • 要么在componentDidUpdate生命周期函数里面获取;
  • 要么就在event loop的最后去获取。比如在event listener中,用setTimeout 来获取:
setTimeout(() => {
    console.log('into setTimeout',this.state.count);
}, 0);

同样的示例代码,我们用reactv16.8.6来跑跑会是怎样的结果呢?:

import React from 'react';

class Count extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  render() {
    return <button onClick={()=> {
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      this.setState({count: this.state.count + 1});
      console.log(this.state.count);
      }}>{this.state.count}</button>
  }

  componentDidMount() {
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
  }
}

export default Count; 

结果是componentDidMount执行后,按钮上的数字是1,控制台打印:

0
0
0

接着click按钮,按钮数字在原来的基础上增加1,变为2,控制台打印:

1
1
1

也就是说在reactv16.8.6版本中,setState都是异步执行的。其实这个说法也不太严谨。因为就像我们上面说的,异步执行只是果,因是批量更新。在react中,要想setState是异步执行的,那么就必须当前是处于“批量更新”事务(transaction)中,而把setState调用写在javascript异步代码中(比如setTimeout,promise等等)是能够逃脱react的这个“批量更新”事务,此时setState是同步执行的。就以上reactv16.8.6版本的示例的event listener中,我们把多次的setState调用用setTimeout包起来试一下?

import React from 'react';

class Count extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }

  render() {
    return <button onClick={()=> {
      setTimeout(()=> {
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
    }, 0);
      }}>{this.state.count}</button>
  }

  componentDidMount() {
    setTimeout(()=> {
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
        this.setState({count: this.state.count + 1});
        console.log(this.state.count);
    }, 0);
  }
}

export default Count; 

同样的操作,结果是什么呢?结果是按钮的数字最终显示为6,打印结果如下:

1
2
3
4
5
6

也就是说,被setTimeout包裹的setState是同步执行的。所以说,“reactv0.13.0版本以后setState都是异步执行”这个说法也是不够严谨的,我们只能说正常情况下是异步执行的。

批量更新的原理

好,言归正传。在简单地介绍setState机制的变更史后,下面我们正式深入探究setState机制在reactv0.8.0的实现。

首先,我们来看看setState方法的实现是如何的(在ReactCompositeComponent.js里面):

/**
   * Sets a subset of the state. Always use this or `replaceState` to mutate
   * state. You should treat `this.state` as immutable.
   *
   * There is no guarantee that `this.state` will be immediately updated, so
   * accessing `this.state` after calling this method may return the old value.
   *
   * There is no guarantee that calls to `setState` will run synchronously,
   * as they may eventually be batched together.  You can provide an optional
   * callback that will be executed when the call to setState is actually
   * completed.
   *
   * @param {object} partialState Next partial state to be merged with state.
   * @param {?function} callback Called after state is updated.
   * @final
   * @protected
   */
  setState: function(partialState, callback) {
    // Merge with `_pendingState` if it exists, otherwise with existing state.
    this.replaceState(
      merge(this._pendingState || this.state, partialState),
      callback
    );
  }

setState方法的实现非常简单,没有想象中的高逼格代码。它先把传进来的partialState跟正在处理中的state(_pendingState)或者当前state进行一个对象的浅合并(shallow merges,即对象最外层的key的覆盖)。然后就把它传给了replaceState方法。而replaceState方法的实现又是这样的:

 /**
   * Replaces all of the state. Always use this or `setState` to mutate state.
   * You should treat `this.state` as immutable.
   *
   * There is no guarantee that `this.state` will be immediately updated, so
   * accessing `this.state` after calling this method may return the old value.
   *
   * @param {object} completeState Next state.
   * @param {?function} callback Called after state is updated.
   * @final
   * @protected
   */
  replaceState: function(completeState, callback) {
    validateLifeCycleOnReplaceState(this);
    this._pendingState = completeState;
    ReactUpdates.enqueueUpdate(this, callback);
  },

replaceState方法里面,第一步先验证一下LifeCycleState,给出一些警告,这个功能没关系,所以略过不表;第二步,把setState方法浅合并后得到的对象变为待处理中的state(_pendingState);而第三步才是开启setState机制大门的钥匙,即这么一句代码:

ReactUpdates.enqueueUpdate(this, callback);

“enqueueUpdate”可以翻译为“推入队列,等待更新”。这个队列其实就是dirtyComponents,这个马上我们就能在enqueueUpdate方法的实现代码中看到。enqueueUpdate方法的实现代码如下(在ReactUpdates.js里面):

/**
 * 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, callback) {
  ("production" !== process.env.NODE_ENV ? invariant(
    !callback || typeof callback === "function",
    'enqueueUpdate(...): You called `setProps`, `replaceProps`, ' +
    '`setState`, `replaceState`, or `forceUpdate` with a callback that ' +
    'is not callable.'
  ) : invariant(!callback || typeof callback === "function"));

  ensureBatchingStrategy(); 

  if (!batchingStrategy.isBatchingUpdates) {
    component.performUpdateIfNecessary();
    callback && callback();
    return;
  }

  dirtyComponents.push(component);

  if (callback) {
    if (component._pendingCallbacks) {
      component._pendingCallbacks.push(callback);
    } else {
      component._pendingCallbacks = [callback];
    }
  }
}

首先提醒“批量更新策略对象”必须注入,否则给出警告。react中有一个默认的批量更新策略对象的实现,这个实现代码是在ReactDefaultBatchingStrategy.js文件中,它长这样:

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  /**
   * Call the provided function in a context within which calls to `setState`
   * and friends are batched such that components are not updated unnecessarily.
   */
  batchedUpdates: function(callback, param) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) { // alreadyBatchingUpdates的值什么情况为true,这是一个值得研究的问题
      callback(param);
    } else {
      transaction.perform(callback, null, param);
    }
  }
};

而注入在哪里做了呢?react中的依赖注入都是在ReactDefaultInjection.js中完成的。这个文件我们在《深入react合成事件系统》中也提到过。在这里,我只是摘取跟本文主题相关的代码出来:

 ReactUpdates.injection.injectBatchingStrategy(
    ReactDefaultBatchingStrategy
  );

我们现在回到enqueueUpdate方法的源码的讨论中来。从源码中可以看出,如果batchingStrategy对象的isBatchingUpdates标志位为false的话,那么我们就直接进入界面更新流程:

if (!batchingStrategy.isBatchingUpdates) {
    component.performUpdateIfNecessary();
    callback && callback();
    return;
  }

否则的话,react就把当前调用了setState方法的组件实例放入到dirtyComponents中,并且把setState方法的callback函数保存在组件实例的_pendingCallbacks字段上:

dirtyComponents.push(component);

  if (callback) {
    if (component._pendingCallbacks) {
      component._pendingCallbacks.push(callback);
    } else {
      component._pendingCallbacks = [callback];
    }
  }

其实如果我们把代码写成if...else...的形式,更能看出代码执行的走向:

if (!batchingStrategy.isBatchingUpdates) {
    component.performUpdateIfNecessary();
    callback && callback();
} else {
    dirtyComponents.push(component);

    if (callback) {
        if (component._pendingCallbacks) {
          component._pendingCallbacks.push(callback);
        } else {
          component._pendingCallbacks = [callback];
        }
    }
}

在继续往下探究前,我先讨论一下dirtyComponents到底是什么。

首先,从数据结构来看,它就是一个数组:

var dirtyComponents = [];

其次,我们要弄清楚,“脏组件”到底是什么意思。

/**
 * Mark a component as needing a rerender, adding an optional callback to a
 * list of functions which will be executed once the rerender occurs.
 */

结合以上源码给出的注释和自己的研究,我可以给出“脏组件”的定义:

如果一个组件的更新(rerender)无法以同步的方式进行,我们就说这个组件是“脏组件”。

如果我们能换个角度来理解“setState”这个方法名,那么“脏组件”这个概念也许更好理解。其实,就本人的理解而言,“setState”不应该叫setState,而是应该叫“requestToSetState”。也就是当用户调用了setState方法是,其本质只是请求react来帮我们马上去更新界面。setState语义上理解是请求更新,实质上它也做了一件事,那就是把传进来的partialState和原来的state进行了对象浅合并,最后赋值给了组件实例的_pendingState。因为react不会马上答应我们的更新请求,而此时组件又处于被“_pendingState”所膈应着的状态。这种情况下,老外们称之为“脏”的。我们中国人也应该能理解。

那么,问题就来了,为什么react不会马上答应我们的更新请求呢?其实在上面给出的enqueueUpdate方法的源码中,我们能找到问题的答案。那就是看批量更新的标志位(isBatchingUpdates)是否为true。

“isBatchingUpdates”,字面意思已经很直白了:“是否处于批量更新中”,这里就不多说了。接下来我们继续自我追问:“那isBatchingUpdates的值什么时候为true呢?”

我们不妨全局搜索一下,一番猛操作后就会发现对“isBatchingUpdates”赋值的语句都是在ReactDefaultBatchingStrategy.js文件里面:

第一个地方是:

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,
  // ......
};

第二个地方同样是在这个ReactDefaultBatchingStrategy对象里面,具体是在它的batchedUpdates方法定义里面:

  batchedUpdates: function(callback, param) {
    // ......
    ReactDefaultBatchingStrategy.isBatchingUpdates = true; 
    // ......
  }
};

第三个地方是在ReactDefaultBatchingStrategyTransaction的一个叫RESET_BATCHED_UPDATES的wrapper的close方法里面:

var RESET_BATCHED_UPDATES = {
  // ......
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false; 
  }
};

结合以上搜索结果和对transaction模式的理解,我们不难看出“isBatchingUpdates”的赋值流程是这样的:

  1. 在初始化ReactDefaultBatchingStrategy对象时,赋予默认值false;
  2. 第三方调用batchedUpdates方法时候,赋值为true;
  3. 在ReactDefaultBatchingStrategyTransaction结束后,赋值为false;

那到底是谁在哪里调用了batchedUpdates方法呢?经过全局搜索,我们发现只有两处batchedUpdates方法的调用:

// 在ReactUpdates.js里面
function batchedUpdates(callback, param) {
  ensureBatchingStrategy();
  batchingStrategy.batchedUpdates(callback, param);
}
// 在ReactUpdates.js里面
 handleTopLevel: function(
      topLevelType,
      topLevelTarget,
      topLevelTargetID,
      nativeEvent) {

    var events = EventPluginHub.extractEvents(
      topLevelType,
      topLevelTarget,
      topLevelTargetID,
      nativeEvent
    );

    ReactUpdates.batchedUpdates(runEventQueueInBatch, events);
  }

而经过代码追溯,我们发现handleTopLevel方法里面的ReactUpdates.batchedUpdates()调用才是真正的调用入口。因为实际的引用传递过程是这样的(“A -> B” 表示“A 把自己的引用传递给B”):

ReactDefaultBatchingStrategy.batchedUpdates -> batchingStrategy.batchedUpdates  ->  
ReactUpdates.batchedUpdates

而batchingStrategy与ReactDefaultBatchingStrategy的传递是通过前面所提到的依赖注入来完成的:

 ReactUpdates.injection.injectBatchingStrategy(
    ReactDefaultBatchingStrategy
  );

handleTopLevel方法是我们的老朋友了。我在《深入react合成事件系统》中也提到过它。读过这篇文章的读者可能知道有这么一个调用链:topLevelCallback() -> handleTopLevel() -> event listener();而我们的setState调用又是在event listener进行的。如今,完整的调用链应该是这样的(“A -> B” 表示“A调用B”):

topLevelCallback() ->  
handleTopLevel() -> 
ReactUpdates.batchedUpdates() -> 
runEventQueueInBatch()  -> 
event listener()  -> 
setState()

到了这里,相信你也看明白了为什么在event listener调用setState是异步执行的吧。那是因为在执行setState前,先执行了ReactUpdates.batchedUpdates(),而ReactUpdates.batchedUpdates()调用就是开启批量更新模式之所在

如果说component.performUpdateIfNecessary();调用后接下来就是界面更新流程。那么放在dirtyComponents数组中组件什么时候会进入这个流程呢?在源码中,这个问题的答案还是有点隐蔽的。如果你对transaction有比较深入的了解,你可能就会比较快地找到答案。因为,答案就在transaction里面,更具体点就是一个叫ReactDefaultBatchingStrategyTransaction的transaction里面。那下面说说我是怎么发现的吧。

ReactUpdates.batchedUpdates的引用指向ReactDefaultBatchingStrategy.batchedUpdates,上面已经说明白了。而ReactDefaultBatchingStrategy.batchedUpdates的实现代码值得我们好好重温一下:

batchedUpdates: function batchedUpdates(callback, param) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    
    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      callback(param);
    } else {
      transaction.perform(callback, null, param);
    }
}

因为一种类型的事件的触发只会导致一次ReactUpdates.batchedUpdates方法的调用并且ReactDefaultBatchingStrategy.isBatchingUpdates的初始值是为false的(也就是说alreadyBatchingUpdates第一次的读取到的值是false的),所以在一次的“event loop”里面,batchedUpdates方法中的if条件分支语句根本就不会被执行到。上面注释也提到了这么写的原因是:“The code is written this way to avoid extra allocations”。所以,对batchedUpdates方法的调用我们只需要注意这条语句就是:

transaction.perform(callback, null, param);

到这里,我们就看到transaction了。翻看ReactDefaultBatchingStrategy.js的源码,我们就会知道这个transaction就是ReactDefaultBatchingStrategyTransaction。也就是说,我们的event listener是在这个ReactDefaultBatchingStrategyTransaction事务中执行的。因为setState调用是在event listener里面,所以setState调用也是在这个事务中。react中所采用的transaction模式是一种类似夹心饼干的模式-用一个个wrapper“包裹”着核心方法,上层是initialize方法,底层是close方法。它运行时的模型大概是这样的:

 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+

那么具体到ReactDefaultBatchingStrategyTransaction中,它的运行时模型具体化如下

--------FLUSH_BATCHED_UPDATES.emptyFunction-------
|
|  -----RESET_BATCHED_UPDATES.emptyFunction------
|  |
|  |    runEventQueueInBatch
|  |
--------FLUSH_BATCHED_UPDATES.close------ // 这个close方法是指向ReactUpdates.flushBatchedUpdates方法
   |    
   -------FLUSH_BATCHED_UPDATES.close-------
        // 上面这个close方法是指向以下函数:
        function() {
            ReactDefaultBatchingStrategy.isBatchingUpdates = false; // 事务完成后,把批量更新开关重置为false
          }

上面的模型是从上到下执行的。 执行完runEventQueueInBatch方法,也就执行完setState调用了。跟着执行的是FLUSH_BATCHED_UPDATES这个wrapper的close方法:ReactUpdates.flushBatchedUpdates。而这个方法就是我们正在寻找答案的所在。那么,下面我们开看看这个ReactUpdates模块的flushBatchedUpdates方法的具体实现:

// 在我们的老朋友ReactUpdates.js文件中
function flushBatchedUpdates() {
  // Run these in separate functions so the JIT can optimize
  try {
    runBatchedUpdates();
  } catch (e) {
    // IE 8 requires catch to use finally.
    throw e;
  } finally {
    clearDirtyComponents();
  }
}

而runBatchedUpdates方法的具体实现又是怎样的呢:

function runBatchedUpdates() {
  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.

  dirtyComponents.sort(mountDepthComparator);

  for (var i = 0; i < dirtyComponents.length; i++) {
    // If a component is unmounted before pending changes apply, ignore them
    // TODO: Queue unmounts in the same list to avoid this happening at all
    var component = dirtyComponents[i];
    if (component.isMounted()) {
      // If performUpdateIfNecessary happens to enqueue any new updates, we
      // should not execute the callbacks until the next render happens, so
      // stash the callbacks first
      var callbacks = component._pendingCallbacks;
      component._pendingCallbacks = null;
      component.performUpdateIfNecessary();
      if (callbacks) {
        for (var j = 0; j < callbacks.length; j++) {
          callbacks[j].call(component);
        }
      }
    }
  }
}

装了24k钛合金眼睛的你看见了没?我们看到了熟悉的身影:dirtyComponentscomponent.performUpdateIfNecessary();。是的,到这里我们可以回答我们之前提出的的问题了。

第一个问题点是:“谁?” 答曰:“是runBatchedUpdates方法”。

第二问题点是:“什么时候?” 答曰:“在ReactDefaultBatchingStrategyTransaction快要结束,执行FLUSH_BATCHED_UPDATES wrapper的close方法的时候”。

到这里,setState无论是同步执行还是异步执行,它们的执行过程中都会到达它们的交汇点了。这个交汇点就是:

component.performUpdateIfNecessary();

从方法名上来顾名思义,从这个交汇点开始后面的流程应该就是界面更新的流程了。界面更新机制会另外撰文阐述,这里就不展开说了。

四个课题

从这个交汇点往前看,我们算是弄明白了“为什么放在event listener里面的setState是异步执行的,为什么放在componentDidMount的setState是同步执行的?”这个问题了。那么,从这个交汇点往后看,我们的问题是什么呢?答曰,我们的问题是“多次调用setState到底发生了什么?”。下面我们来探究一下。

多次调用setState又可以细分为两种场景,两种模式来研究。所以,我们会有以下的四个研究课题:

  1. 批量更新模式下,在同一个方法里面连续多次调用setState
  2. 批量更新模式下,在组件树更新过程中,不同层级的组件调用setState
  3. 非批量更新模式下,在同一个方法里面连续多次调用setState
  4. 非批量更新模式下,在组件树更新过程中,不同层级的组件调用setState

下面,每个课题结合一个具体的示例代码进行一一研究。

课题1

const Parent = React.createClass({
    getInitialState(){
        return {
            count: 0
        }
    },
    handleClick(){
        this.setState({
            count: this.state.count +  1
        });
        this.setState({
            count: this.state.count +  1
        });
        this.setState({
            count: this.state.count +  1
        });
    },
    render(){
        return react.DOM.button({
            onClick: this.handleClick
        },`I have been clicked ${this.state.count} times`)
    }
})

上面handleClick方法中的多次setState调用因为是处于批量更新模式中,所以它们的执行是异步的。这个因果关系在文章上面已经讲述清楚了,这里赘述了。这里,我们都是为了搞清楚多次调用setState之后到底发生什么。

在批量更新模式中,多次调用setState之后发生的事件也简单地提到过。那就是,首先做state的对象浅合并,并将合并结果更新到组件实例的_pendingState 属性,然后把当前组件实例push到dirtyComponents中去。就这个例子而言,因为三个this.state.count的访问值都是0,三个{count: this.state.count + 1}的浅合并的结果是:

{
    count: 1
}

之后就把Parent组件的实例推入到dirtyComponents中去了。因为调用一次setState,就推一次,所以dirtyComponents组件中应该有三个相同的组件实例。我们不妨把dirtyComponents打印出来看看:

在reactv0.8.0里面,组件实例长这样的:

(看到没,_pendingState为{count: 1}在等着我们呢)好了,此时dirtyComponents已经准备完毕了。它会在ReactDefaultBatchingStrategyTransaction执行close方法的时候flush掉。负责flush的方法上面提到过:

function flushBatchedUpdates() {
  // Run these in separate functions so the JIT can optimize
  try {
    runBatchedUpdates();
  } catch (e) {
    // IE 8 requires catch to use finally.
    throw e;
  } finally {
    clearDirtyComponents();
  }
}

而runBatchedUpdates方法的源码上面也给出过:

function runBatchedUpdates() {
  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.

  dirtyComponents.sort(mountDepthComparator);

  for (var i = 0; i < dirtyComponents.length; i++) {
    // If a component is unmounted before pending changes apply, ignore them
    // TODO: Queue unmounts in the same list to avoid this happening at all
    
    var component = dirtyComponents[i];
    if (component.isMounted()) {
      // If performUpdateIfNecessary happens to enqueue any new updates, we
      // should not execute the callbacks until the next render happens, so
      // stash the callbacks first
      
      var callbacks = component._pendingCallbacks;
      component._pendingCallbacks = null;
      component.performUpdateIfNecessary();
      if (callbacks) {
        for (var j = 0; j < callbacks.length; j++) {
          callbacks[j].call(component);
        }
      }
    }
  }
}

在本课题中,我们先不管dirtyComponents.sort(mountDepthComparator);,因为它属于下个课题要研讨的问题。从源码中可以看出,接下来要做的是遍历dirtyComponents数组,逐一逐一调用它们的performUpdateIfNecessary方法。

在react中,调和更新(reconciliation)是一个从顶层组件开始,递归到最底层组件不间断的过程。我称这个更新过程是“一镜到底的更新”。

“一镜到底的更新”其实又可以划分为四个阶段:

  1. 请求更新
  2. 决定是否同意该请求
  3. 从父组件到子组件的递归遍历
  4. 真正的DOM操作

从performUpdateIfNecessary方法名中的“IfNecessary”,我们就能猜到这个方法是属于第二个阶段。那到底react是如何决定是否同意更新请求呢?

我们沿着第二阶段所涉及的调用栈,可以找到答案(调用栈中的调用顺序是从下往上看):

下面我们把上面的调用栈再明确一下调用的上下文(这里的调用顺序是从上到下看):

ReactCompositeComponent.performUpdateIfNecessary ->
ReactComponent.Mixin.performUpdateIfNecessary ->
ReactCompositeComponent._performUpdateIfNecessary ->
ReactCompositeComponent._performComponentUpdate

如果你仔细沿着这段调用栈去看源码,你会发现react决定是否同意更新请求的判断依据有三个,我们可以理解为三个通关关卡。

第一个在ReactCompositeComponent.performUpdateIfNecessary方法中:

// ......
if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING || compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
  return;
}
// ......

第二个和第三个在ReactCompositeComponent._performUpdateIfNecessary方法中:

if (this._pendingProps == null && this._pendingState == null && !this._pendingForceUpdate) {
  return;
}
if (this._pendingForceUpdate || !this.shouldComponentUpdate || this.shouldComponentUpdate(nextProps, nextState)) {
  this._pendingForceUpdate = false;
  // Will set `this.props` and `this.state`.
  this._performComponentUpdate(nextProps, nextState, transaction);
} else {
  // If it is not determined that a component should not update, we still want
  // to set props and state.
  this.props = nextProps;
  this.state = nextState;
}

现在我们逐个逐个地过过这些判断依据。

对于第一次调用的component.performUpdateIfNecessary();,在本示例中,因为Parent组件是本次更新的顶层组件且是由调用setState而引起的更新,所以它的CompositeLifeCycle状态值不可能为CompositeLifeCycle.RECEIVING_PROPS。又因为这是组件更新,不是初始挂载,所以compositeLifeCycleState状态值不可能为MOUNTING。所以,第一个关卡通过。

我们来看看第二个关卡。this._pendingProps是为null,原因同上。但是this._pendingState可不为null。因为三次连续的setState的调用,在批量更新模式下,实质是对象的浅合并,所以,此时this._pendingState的值是为{count: 1},所以,第二关卡也通过了。

继续看看第三个关卡。因为我们并不是调用forceUpdate方法来更新界面的,所以this._pendingForceUpdate的值为false;因为我们并没有挂载shouldComponentUpdate这个生命周期函数,!this.shouldComponentUpdate的值为true,所以我们又顺利地通过第三个关卡。

通过三个关卡后,调用this._performComponentUpdate方法,正式进入后续的界面更新流程。

综上所述,在本示例中,第一个setState调用是会进行一个完整的“一镜到底的更新”。

组件树完成了一次“一镜到底的更新”后,我们就接着调用第二个component.performUpdateIfNecessary()。在这里,值得指出的是,三次推进dirtyComponents的组件实例其实是同一个。不信?我们在runBatchedUpdates方法里面打个log看看:

function runBatchedUpdates() {
 // ......
  console.log(dirtyComponents[0] === dirtyComponents[1], dirtyComponents[1] === dirtyComponents[2]);  
 // .....
}

打印结果:

true true

因此,在第一次setState调用时,在它成功通过第二个关卡后,是有一个清除_pendingState的操作(在ReactCompositeComponent._performUpdateIfNecessary方法中):

_performUpdateIfNecessary: function(transaction) {
   // ......
   var nextState = this._pendingState || this.state;
   this._pendingState = null; 
   // ......
}

又因为dirtyComponents中三个组件实例是同一所指,所以,在第二次调用component.performUpdateIfNecessary()所对应的通关检测的第二关卡是过不的了。用代码来说就是,this._pendingProps == null && this._pendingState == null && !this._pendingForceUpdate的值为true(主要是因为this._pendingState的值为null),因此执行return语句,“一镜到底的更新”的流程到这里就戛然而止了。

同理,第三个的component.performUpdateIfNecessary()调用也是一样的,这里就不赘述了。我们在ReactCompositeComponent._performUpdateIfNecessary方法中写个log来佐证一下:

console.log('into _performUpdateIfNecessary:', this._pendingProps == null && this._pendingState == null && !this._pendingForceUpdate);

针对本示例代码,打印结果是:

into _performUpdateIfNecessary: false
into _performUpdateIfNecessary: true
into _performUpdateIfNecessary: true

综上所述,在事件处理器中的连续三次的setState调用,本质上就是首先做了三次的对象浅合并,最后在组件树上做了一次的“一镜到底的更新”,而后面两次的setState发起的更新流程都是中途而废,戛然而止。也许,这就是react中“批量更新”的更具象化的含义。

课题2

const Child = React.createClass({
    getInitialState(){
        return {
            count: 0
        }
    },
    handleClick(){
        this.setState({
            count: this.state.count +  1
        });
    },
    render(){
        return react.DOM.button({
            onClick: this.handleClick
        },`I have been clicked ${this.state.count} times`)
    }
});

const Parent = React.createClass({
    getInitialState(){
        return {
            count: 0
        }
    },
    handleClick(){
        this.setState({
            count: this.state.count +  1
        });
    },
    render(){
        return react.DOM.div({
            onClick: this.handleClick
        },Child())
    }
});

在这个课题里面,我们聊聊同一个组件树里面,不同层级组件多次调用setState的情况。

我们可以看到ParentChild组件都有个handleClick这个事件处理器。它们都注册在事件冒泡阶段。因此,当用户一个click事件发生时,Child.handleClick先被调用,Parent.click后被调用。又因为当前是处在“批量更新模式”下,作为结果,Child组件实例先被push到dirtyComponents,Parent组件实例后被push进去:

基于这个结果去理解接下来的更新流程的话,你可能觉得在flush dirtyComponents的时候,会先做Child组件的更新,再做Parent组件的更新。其实不然。因为在执行flush dirtyComponents的之前会有一个根据组件层级进行排序的操作。这就是我们研究上一个问题所提到的dirtyComponents.sort(mountDepthComparator)问题:

function runBatchedUpdates() {
  // Since reconciling a component higher in the owner hierarchy usually (not
  // always -- see shouldComponentUpdate()) will reconcile children, reconcile
  // them before their children by sorting the array.

  dirtyComponents.sort(mountDepthComparator);
  // ......
}

这行代码十分关键,通过对dirtyComponents的排序,react保持了reconciliation流程的顺序总是从组件树的根组件到最底层的组件。值得一提的是排序所依据的组件实例“_mountDepth”字段。这个字段值是一个编号,表示的是当前组件在组件树的层级。编号工作是从根组件开始的,并以0为基数,一层层地自增。从上面给出的截图,我们可以看到,parent组件的_mountDepth值为0,child组件的_mountDepth值为2。

重温数组的sort方法的使用:升序:sort((a,b) => a - b);降序: sort((a,b) => b - a)

从源码中mountDepthComparator函数的实现:

function mountDepthComparator(c1, c2) {
  return c1._mountDepth - c2._mountDepth;
}

我们可以看出react是根据_mountDepth的值对dirtyComponents进行升序排序的,从而保证了根组件始终是处于数组的第一位:

也就是说,在“批量更新模式”下,无论在组件树中不同层级上的组件以何种次序调用setState,最终在flush dirtyComponents之前,都会进行重排。所以,在本示例中,react会先尝试更新Parent组件,再是Child组件。 “批量更新模式”下,组件的的更新总是先更新所有组件的state(准确地说是更新_pendingState), 再尝试去做真正的DOM层面的更新。关于这点,相信完整看过上文的人都会大概知道的。

在示例中,react会先尝试在更新Parent组件。对照一下上个示例中的Parent组件,因为都是处于“批量更新模式”下,并且都是根组件,所以本示例中的Parent组件的更新同样是一个“一镜到底的更新”的。

因为Parent组件的“一镜到底的更新”会把整一颗的组件树所有的组件都更新了,Child组件也不例外,它的“_pendingState”字段也会被清空。当轮到flush自己的时候,Child组件因为此时的this._pendingState为null,所以,Child组件的更新流程是无法通过第二关卡的。

到目前为止,本示例多次调用setState所发生的事情其实跟示例1所发生的事情是一样的,即:先是state的浅合并,更新到 _pendingState,然后一次的“一镜到底的更新”,而后的所有发起的更新流程都会终止于第二关卡。

下面,我们不妨加大点研究的难度。怎么个加大法呢?在Child组件中加入个生命周期函数componentWillReceiveProps:


    const Child = React.createClass({
    getInitialState(){
        return {
            count: 0
        }
    },
    handleChildClick(){
        this.setState({
            count: this.state.count +  2
        });
    },
    componentWillReceiveProps(){
        this.setState({
            count: 10
        });
    },
    render(){
        return React.DOM.button({
            onClick: this.handleChildClick
        },`Child count ${this.state.count} times`)
    }
});

加入componentWillReceiveProps之后,带来了三个问题:

  1. handleChildClick和componentWillReceiveProps的setState调用到底谁先谁后?
  2. 最终Child组件的state值是多少?
  3. 本示例的componentWillReceiveProps中调用setState到底发生了什么?

下面我们一一来回答。

我们从用户的click动作发生时开始算,执行流程是这样:

  1. 执行handleChildClick,更新Child组件的_pendingState;
  2. 执行handleParentClick,更新Parent组件的_pendingState;
  3. Parent组件进行“一镜到底的更新”过程中会更新Child组件,此时会调用componentWillReceiveProps。
  4. 调用componentWillReceiveProps()时,调用setState再次更新Child组件的_pendingState。

问题1的答案:

从上面这个流程来看,是先执行handleChildClick的setState,后执行componentWillReceiveProps的setState。

问题2的答案:

从负责调用调用componentWillReceiveProps生命周期函数的ReactCompositeComponent._performUpdateIfNecessary的实现源码中:

 _performUpdateIfNecessary: function(transaction) {
    if (this._pendingProps == null &&
        this._pendingState == null &&
        !this._pendingForceUpdate) {
      return;
    }

    var nextProps = this.props;
    if (this._pendingProps != null) {
      nextProps = this._pendingProps;
      this._processProps(nextProps);
      this._pendingProps = null;

      this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
      if (this.componentWillReceiveProps) {
        this.componentWillReceiveProps(nextProps, transaction);
      }
    }

    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

    var nextState = this._pendingState || this.state;
    this._pendingState = null;

    if (this._pendingForceUpdate ||
        !this.shouldComponentUpdate ||
        this.shouldComponentUpdate(nextProps, nextState)) {
      this._pendingForceUpdate = false;
      // Will set `this.props` and `this.state`.
      this._performComponentUpdate(nextProps, nextState, transaction);
    } else {
      // If it not determined that a component should not update, we still want
      // to set props and state.
      this.props = nextProps;
      this.state = nextState;
    }

    this._compositeLifeCycleState = null;
  },

我们可以看到,this.componentWillReceiveProps(nextProps, transaction);语句是在var nextState = this._pendingState || this.state;之前,而问题1的答案也说了,setState({count: this.state.count + 1}) 又在setState({count: 10})之前,也就是说this._pendingState的值会经历两次对象浅合并后成为更新Child组件所要用到的nextState。所以,最后Child组件的state值是{count: 10}

问题3的答案:

因为componentWillReceiveProps()的调用是发生在Parent组件的“一镜到底的更新”过程中,而Parent组件的“一镜到底的更新”又是发生在“批量更新模式”下,所以,componentWillReceiveProps里面的setState也是跟handleChildClick里面的setState一样,都是异步执行,具体表现是:合并state,并把合并后的state值更新到_pendingState上,然后往dirtyComponents里面push当前的组件实例。最后,还是等当前的“一镜到底的更新”完成后,flush dirtyComponents工作遍历到自己时再请求更新自己。同样,因为无法通过第二关卡,因此注定是徒劳的。

在没有对本示例进行研究前,我们直觉上可能觉得更新是这样发生的:

更新Child组件  ->  更新Parent组件  ->  更新Child组件

经过一番梳理后,我们发现它实际是这样发生的:

先更新Child组件的_pendingState ->  再更新Parent组件的_pendingState  ->  最后在Parent组件的“一镜到底更新”过程中更新Child组件

课题3

const Parent = React.createClass({
    getInitialState(){
        return {
            count: 0
        }
    },
    render(){
        return React.DOM.button({},`I have been clicked ${this.state.count} times`)
    },
    componentDidMount() {
        this.setState({
            count: this.state.count + 1
        });
         this.setState({
            count: this.state.count + 1
        });
         this.setState({
            count: this.state.count + 1
        });
    }
})

经过对上面两个课题的研究,相信我们也不用费太多口舌来阐述本示例中setState的多次调用发生了什么了。因为当前的setState调用是处于“批量更新模式”下,所以,三个setState是实质上相当于三个 "component.performUpdateIfNecessary()"调用。因为中途没有其他子组件请求更新,所有也就是说会依次发生三个Parent组件的“一镜到底更新”。我们在componentDidUpdate 这个生命周期函数打个log来佐证一下:

componentDidUpdate(){
    console.log('into componentDidUpdate');
}

如果所见,事实也是发生了三次组件更新。

课题4

    const Child = React.createClass({
    getInitialState(){
        return {
            count: 0
        }
    },
    componentWillReceiveProps(){
        this.setState({
            count: 10
        });
    },
    render(){
        return React.DOM.button({
            onClick: this.handleChildClick
        },`Child count ${this.state.count} times`)
    }
});

const Parent = React.createClass({
    getInitialState(){
        return {
            count: 0
        }
    },
    render(){
        return React.DOM.div({},`I have been clicked ${this.state.count} times`)
    },
    componentDidMount() {
        this.setState({
            count: this.state.count + 1
        });
    }
})

很明显,当前不是“批量更新模式”下,所有Parent组件的setState调用会引起一次“一镜到底更新”。本示例中,我们的重点是研究Child组件在生命周期函数componentWillReceiveProps调用setState到底发生了什么吗?会不会导致一次以Child组件为根组件的“一镜到底更新”呢?下面,我们继续进行探索。

我们去调用componentWillReceiveProps函数的ReactCompositeComponent._performUpdateIfNecessary方法的源码看看:

_performUpdateIfNecessary: function(transaction) {
    if (this._pendingProps == null &&
        this._pendingState == null &&
        !this._pendingForceUpdate) {
      return;
    }

    var nextProps = this.props;
    if (this._pendingProps != null) {
      nextProps = this._pendingProps;
      this._processProps(nextProps);
      this._pendingProps = null;

      this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_PROPS;
      if (this.componentWillReceiveProps) {
        this.componentWillReceiveProps(nextProps, transaction);
      }
    }

    this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

    var nextState = this._pendingState || this.state;
    this._pendingState = null;

    if (this._pendingForceUpdate ||
        !this.shouldComponentUpdate ||
        this.shouldComponentUpdate(nextProps, nextState)) {
      this._pendingForceUpdate = false;
      // Will set `this.props` and `this.state`.
      this._performComponentUpdate(nextProps, nextState, transaction);
    } else {
      // If it is not determined that a component should not update, we still want
      // to set props and state.
      this.props = nextProps;
      this.state = nextState;
    }

    this._compositeLifeCycleState = null;
  },

我们看到在调用componentWillReceiveProps生命周期函数前有一个对组件的生命周期状态赋值的操作:

this._compositeLifeCycleState = CompositeLifeCycle.RECEIVING_STATE;

这行代码很重要,先记下来。因为当前并不处于“批量更新模式”中,所以componentWillReceiveProps中调用setState,先做_pendingState的更新,最终还是回到上面我们提到的交汇点:component.performUpdateIfNecessary();。这是我们的老朋友了,上面示例中反复提到它了。它是组件进行真正DOM层面更新流程的起点。从这个起点之后,会遇到三道关卡,上面也是提到过的。

我们不妨从第一关卡(在ReactCompositeComponent.performUpdateIfNecessary方法中)开始看:

// ......
if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING || compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
  return;
}
// ......

仔细端详一下,我们发现里面有这么一个条件compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS。而往上几行文字,我们提到过,在调用componentWillReceiveProps之前,其实已经对compositeLifeCycleState赋了值,这个值正好是CompositeLifeCycle.RECEIVING_PROPS。所以,这里的答案已经是显而易见了。就是说,刚好满足条件,然后执行return语句。第一关卡就无法通过了,所以,我们在研究本示例之前推测的“会不会导致一次以Child组件为根组件的“一镜到底更新”呢”的答案是:“否”。

总结

一番研究下来,我们可以得出这样的结论(只针对reactv0.8.0):

  • 在react中有两种更新模式:批量更新模式和非批量更新模式。批量更新模式下,setState是异步执行的;非批量更新模式下,setState是同步执行的。
  • 对“批量”的理解应该是这样的:同一个组件的多次更新请求合并为一次的更新请求(即产出一个最终的_pendingState),多个组件的更新请求合并到一次根组件更新(即只有一个“一镜到底更新”)。
  • ReactUpdates.batchedUpdates()调用就是开启批量更新模式之所在。
  • 无论你以何种次序请求更新,react总是能保证更新从高(层组件)到低(层组件)的顺序去更新。
  • react是通过设立三道关卡来保证只有一次的“一镜到底更新”:
1. 第一道:
if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING || compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
  return;
}

2. 第二道:
if (this._pendingProps == null && this._pendingState == null && !this._pendingForceUpdate) {
  return;
}

3. 第三道:
if (this._pendingForceUpdate || !this.shouldComponentUpdate || this.shouldComponentUpdate(nextProps, nextState)) {
  this._pendingForceUpdate = false;
  this._performComponentUpdate(nextProps, nextState, transaction);
} else {
  // If it is not determined that a component should not update, we still want
  // to set props and state.
  this.props = nextProps;
  this.state = nextState;
}

行文至此,已写了8820字,我相信对于setState机制的细节已经摸索得差不多了。我觉得要想理解setState机制的精髓,首先要做的是,要把“setState”理解为“requestToSetState”。因为名不副其实,我一直觉得react团队把这个API命名为“setState”多少是不够恰当的。或许是为名字够简短,who knows.....。

更新于 2022-09-01

image.png

"we don’t think of rendering as a “signal”, more a “request”" 。Dan 如此说道。这也证明了我写下该文章时对 setState() API 的心智模型是对的。也就是说,调用该 API,只是向 react 「发送一个请求」,而不是 setXXX 所对应的「马上生效」的心智模型。

同时我们也要思考引入这种机制的目的是什么?那就是减少不必要的组件渲染。更确切地说是减少不必要的函数调用和DOM操作,使得界面更新的性能更优。

最后,我上面提到的,我们可以把组件的更新流程划分为四个阶段:

  1. 请求更新
  2. 决定是否同意该请求
  3. 从父组件到子组件的递归遍历
  4. 真正的DOM操作

更新流程图如下

RCC:ReactCompositeComponent

RDC:ReactDOMComponent

RTC:ReactTextComponent

显然,本文所研究的setState机制其实只是囊括前面两个阶段。至于后面两个阶段的深入,待择日撰文进行详细阐述吧。

谢谢阅读,好走不送。