Reactjs源码走读 --- setState时触发render的过程

在调用setState时,react会帮我们去更新DOM,重新去render一次,这个过程是怎么实现的呢?了解它就能很好的帮助我们理解为什么有时我们通过setState改变了一个变量的值,而再次使用时还是原来的值。
先来看个例子。

一个点击事件中调用setState

import React from 'react';

export default class RIndex extends React.Component {
    state = {
        text1: 1
    }

    constructor(props) {
        super(props);
        this.change = this.change.bind(this);
    }

    change() {
        this.setState({
            text1: this.state.text1 + 1
        });
        debugger;
    }

    render() {
        return (
            <div>
                <h1>This is RIndex</h1>
                <h3>{this.state.text1}</h3>
                <button onClick={this.change}>change</button>
            </div>
        );
    }
}

我们调用setState时,使用的是this,而这个this是当前react组件,当前react组件继承自React.Componet,所以setState应该是React.Component中定义的方法,找到源码中的React.js文件,一步步找下去会发现,setState的调用路径如下:

/** ====== React.js ====== */
...
Component: ReactBaseClasses.Component
...
/** ====== ReactBaseClasses ====== */
...
ReactComponent.prototype.setState = function(...) {
  ...
  // this.updater是ReactUpdateQueue,找到这个对应关系很简单,只需要全局搜索enqueueSetState方法,
  // 但要弄明白是在哪里赋值的,那就需要花很长时间了
  this.updater.enqueueSetState(this, partialState);
  ...
}
...
/** ====== ReactUpdateQueue ======*/
...
function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}
...
enqueueSetState: function (publicInstance, partialState) {
  enqueueUpdate(internalInstance);
}
...
/** ====== ReactUpdates ======*/
...
function enqueueUpdate(component) {}
...

查看调用路径中的方法,会发现,这个路径中,仅仅是将入参中的state放到一个实例的属性中,以及将当前组件放到ReactUpdates 的全局变量dirtyComponents中,并没有发现真正是在哪里去触发更新DOM的操作。回想整个流程,我们的setState是放在事件回调中的,那么事件触发时会不会做了什么事呢?

关注事件触发代码

找到事件触发的代码

/** ReactEventListener */
dispatchEvent: function (topLevelType, nativeEvent) {
  ...
  ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  ...
}

发现在事件触发的方法中,有一个ReactUpdates.batchedUpdates方法的调用,看名字应该和更新DOM有关系,查看该方法的调用链路

/** ====== ReactUpdates ======*/
function batchedUpdates(callback, a, b, c, d, e) {
  ensureInjected();
  return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
}
/** ====== ReactDefaultBatchingStrategy ======*/
batchedUpdates: function (callback, a, b, c, d, e) {
  // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
}

其中batchingStrategy是在react组件初始化的时候注册进来的,指向ReactDefaultBatchingStrategy对象。ReactDefaultBatchingStrategy对象的batchedUpdates方法,第一个参数callback就是事件触发时传入的handleTopLevelImpl,是真正的事件回调函数,从这个if语句看,应该是若已经update过了,就直接触发callback,若没有update,则调用transaction.perform。
Transaction是react中定义的事务,Transaction文件中有说明以及一个很形象的图:


Transaction

可以看出Transaction就是可以在你想要调用的方法之前及之后包入其他方法,且保证这些外层方法一定会执行。
查看ReactDefaultBatchingStrateg中的transaction的实现

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

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};
// 传入两个wrapper生成transaction
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}
_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  }
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

可以看到ReactDefaultBatchingStrategy中的transaction包了两个wrapper,第一个wrapper的close中传入了ReactUpdates.flushBatchedUpdates,也就是在事件触发后,会调用ReactUpdates.flushBatchedUpdates方法,而flushBatchedUpdates后续的代码才是正直触发了更新DOM的操作。
注:可以好好理解一下Transcation,react源码中大量使用了它。

总结

完整的调用流程为:事件触发 ---> 包装一个事务 ---> 事务在触发事件回调之后执行更新DOM操作。也就是若我们在事件回调中多次调用了setState,实际react是先把这些setState里设置的值全部放到当前组件的_pendingStateQueue对象中,最后再一次调用更新DOM的方法的。
因此,当我们在一个事件回调函数中写了两次setState时,实际只执行了一次更新DOM的操作。如下代码的结果也就不难理解了

state = {
  count: 0
}
// 事件执行一次后,state中的count为1
change() {
  // this.state.count在这里为0
  this.setState({count: this.state.count + 1);
  // this.state.count在这里也为0
  this.setState({count: this.state.count + 1);
}

推荐阅读更多精彩内容