React - setState源码分析(小白可读)

9,753 阅读9分钟

一、请先看官方文档

上来先看官方文档中对setState()的定义 英文文档最佳

React英文文档

React中文文档

二、setState()的实践与问题

先看个最简单的问题,点击按钮后,count是加2吗? 

class NextPage extends Component<Props> {
  static navigatorStyle = {
    tabBarHidden: true
  };

  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  add() {
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
  }

  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity
          style={styles.addBtn}
          onPress={() => {
            this.add();
          }}
        >
          <Text style={styles.btnText}>点击+2</Text>
        </TouchableOpacity>

        <Text style={styles.commonText}>当前count {this.state.count}</Text>
      </View>
    );
  }
}

结果却是1



为什么会只加1?

看官网这句话

setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.

重点是前两句,翻译过来就是 setState()并不总是立即更新组件,它可能会进行批处理或者推迟更新。这使得在调用setState()之后立即读取this.state成为一个潜在的隐患。 先直接抛出点击按钮加2的正确答案吧,下面两种方法都OK

this.setState(preState => {
  return {
    count: preState.count + 1
  };
});
this.setState(preState => {
  return {
    count: preState.count + 1
  };
});

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



三、setState源码世界

相信能到这里的同学都知道了setState()是个`既能同步又能异步`的方法了,那具体什么时候是同步的,什么时候是异步的?

去源码里面看实现是比较靠谱的方式。 

注:这里说的同步和异步只是“实现上看起来像同步还是异步,比如上面答案二setTimeout里面,看起来就是同步的”,实质上setState()还是异步的 不管这里看不看得懂都没关系了,马上进入源码的世界。 

1、如何快速查看react源码

上react的github仓库,直接clone下来

react-github仓库

git clone https://github.com/facebook/react.git


到目前我看为止,最新的版本是16.2.0,我选了15.6.0的代码 

一是为了参考前辈们的分析成果 

二来,我水平有限,如果写的实在不清晰,同学们还可以参考着其他人的分析文章一起读,而不至于完全理解不了

如何切换版本? 

1、找到对应版本号


2、复制15.6.0的历史记录号 


3、回滚

git reset --hard 911603b

如图,成功回滚到15.6.0版本



2、setState入口 => enqueueSetState

核心原则:既然是看源码,那当然就不是一行一行的读代码,而是看核心的思想,所以接下来的代码都只会放核心代码,旁枝末节只提一下或者忽略


setState的入口文件在src/isomorphic/modern/class/ReactBaseClasses.js

React组件继承自React.Component,而setState是React.Component的方法,因此对于组件来讲setState属于其原型方法 

ReactComponent.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

partialState顾名思义-“部分state”,这取名,大概就是想不影响原来的state的意思吧


当调用setState时实际上是调用了enqueueSetState方法,我们顺藤摸瓜(我用的是vscode的全局搜索),找到了这个文件src/renderers/shared/stack/reconciler/ReactUpdateQueue.js


这个文件导出了一个ReactUpdateQueue对象,“react更新队列”,代码名字起的好可以自带注释,说的就是这种大作吧,在这里注册了enqueueSetState方法


3、enqueueSetState => enqueueUpdate

先看enqueueSetState的定义

  enqueueSetState: function(publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'setState',
    );
	
    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  },

这里只需要关注internalInstance的两个属性 

_pendingStateQueue:待更新队列 

_pendingCallbacks: 更新回调队列 

如果_pendingStateQueue的值为null,将其赋值为空数组[],并将partialState放入待更新state队列_pendingStateQueue,最后执行enqueueUpdate(internalInstance)

接下来看enqueueUpdate

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

它执行的是ReactUpdates的enqueueUpdate方法

var ReactUpdates = require('ReactUpdates');

这个文件刚好就在旁边src/renderers/shared/stack/reconciler/ReactUpdates.js

找到enqueueUpdate方法


定义如下

function enqueueUpdate(component) {
  ensureInjected();

  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}


这段代码对于理解setState非常重要

if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
dirtyComponents.push(component);

判断batchingStrategy.isBatchingUpdates batchingStrategy是批量更新策略,isBatchingUpdates表示是否处于批量更新过程,开始默认值为false


上面这句话的意思是: 

如果处于批量更新模式,也就是isBatchingUpdates为true时,不进行state的更新操作,而是将需要更新的component添加到dirtyComponents数组中;

如果不处于批量更新模式,对所有队列中的更新执行batchedUpdates方法,往下看下去就知道是用事务的方式批量的进行component的更新,事务在下面。

借用《深入React技术栈》Page167中一图 



4、核心:batchedUpdates => 调用transaction

那batchingStrategy.isBatchingUpdates又是怎么回事呢?看来它才是关键

但是,batchingStrategy 对象并不好找,它是通过 injection 方法注入的,一番寻找,发现了 batchingStrategy 就是 ReactDefaultBatchingStrategy。 

src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js具体怎么找文件,又属于另一个范畴了,我们今天只专注 setState,其他的容后再说吧 


相信部分同学看到这里已经有些迷糊了,没关系,再坚持一下,旁枝末节先不管,只知道我们找到了核心方法batchedUpdates,马上要胜利了,别放弃(我第一次看也是这样熬过来的,一遍不行就两遍,大不了看多几遍又如何)


先看批量更新策略-batchingStrategy,它到底是什么

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};

module.exports = ReactDefaultBatchingStrategy;

终于找到了,isBatchingUpdates属性和batchedUpdates方法


如果isBatchingUpdates为true,当前正处于更新事务状态中,则将Component存入dirtyComponent中, 否则调用batchedUpdates处理,发起一个transaction.perform()

注:所有的 batchUpdate 功能都是通过执行各种 transaction 实现的

这是事务的概念,先了解一下事务吧


5、事务

这一段就直接引用书本里面的概念吧,《深入React技术栈》Page169



简单地说,一个所谓的 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 的方法。 


下面这段代码应该能帮助理解

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


6、核心分析:batchingStrategy 批量更新策略

回到batchingStrategy:批量更新策略,再看看它的代码实现

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,

  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};


可以看到isBatchingUpdates的初始值是false的,在调用batchedUpdates方法的时候会将isBatchingUpdates变量设置为true。然后根据设置之前的isBatchingUpdates的值来执行不同的流程


还记得上面说的很重要的那段代码吗

if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
dirtyComponents.push(component);

1、首先,点击事件的处理本身就是在一个大的事务中(这个记着就好),isBatchingUpdates已经是true了  


2、调用setState()时,调用了ReactUpdates.batchedUpdates用事务的方式进行事件的处理  


3、在setState执行的时候isBatchingUpdates已经是true了,setState做的就是将更新都统一push到dirtyComponents数组中; 


4、在事务结束的时候才通过 ReactUpdates.flushBatchedUpdates 方法将所有的临时 state merge 并计算出最新的 props 及 state,然后将批量执行关闭结束事务。


到这里我并没有顺着ReactUpdates.flushBatchedUpdates方法讲下去,这部分涉及到渲染和Virtual Dom的内容,反正你知道它是拿来执行渲染的就行了。 

到这里为止,setState的核心概念已经比较清楚了,再往下的内容,暂时先知道就行了,不然展开来讲一环扣一环太杂了,我们做事情要把握核心。 


到这里不知道有没有同学想起一个问题 ?

isBatchingUpdates 标志位在 batchedUpdates 发起的时候被置为 true ,那什么时候被复位为false的呢?

还记得上面的事务的close方法吗,同一个文件src/renderers/shared/stack/reconciler/ReactDefaultBatchingStrategy.js

// 定义复位 wrapper
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function () {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

// 定义批更新 wrapper
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

_assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function () {
    return TRANSACTION_WRAPPERS;
  }
});

相信眼尖的同学已经看到了,close的时候复位,把isBatchingUpdates设置为false。


Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});

var transaction = new ReactDefaultBatchingStrategyTransaction();

通过原型合并,事务的close 方法,将在 enqueueUpdate 执行结束后,先把 isBatchingUpdates 复位,再发起一个 DOM 的批更新  


到这里,我们会发现,前面所有的队列、batchUpdate等等都是为了来到事务的这一步,前面都只是批收集的工作,到这里才真正的完成了批更新的操作。


7、再回到最初的题目

add() {
    this.setState({
      count: this.state.count + 1
    });
    this.setState({
      count: this.state.count + 1
    });
  }

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


第一种情况,在执行第一个setState时,本身已经处于一个点击事件触发的这个大事务中,已经触发了一个batchedUpdates,isBatchingUpdates为true,所以两个setState都会被批量更新,这时候属于异步过程,this.state并没有立即改变,执行setState只是相当于把partialState(前面说的部分state)传入dirtyComponents,最后在事务的close阶段执行flushBatchedUpdates去重新渲染。


第二种情况,有了setTimeout,两次setState都会在点击事件触发的大事务中的批量更新batchedUpdates结束之后再执行,所以他们会触发两次批量更新batchedUpdates,也就会执行两个事务和函数flushBatchedUpdates,就相当于同步更新的过程了。


后话

感谢您耐心看到这里,希望有所收获!

如果不是很忙的话,麻烦点个star⭐【Github博客传送门】,举手之劳,却是对作者莫大的鼓励。

我在学习过程中喜欢做记录,分享的是自己在前端之路上的一些积累和思考,希望能跟大家一起交流与进步,更多文章请看【amandakelake的Github博客】