博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
React setState源码阅读
阅读量:7073 次
发布时间:2019-06-28

本文共 11126 字,大约阅读时间需要 37 分钟。

本文基于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);  // 1this.setState((prevState) => {    return { count: prevState.val + 1 }})console.log(this.state.val);  // 2this.setState((prevState) => {    return { count: prevState.val + 1 }})console.log(this.state.val);  // 3this.setState((prevState) => {    return { count: prevState.val + 1 }})console.log(this.state.val);  // 4复制代码

setState 干了什么

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

// src/isomorphic/modern/class/ReactComponent.jsReactComponent.prototype.setState = function(partialState, callback) {  // ...  this.updater.enqueueSetState(this, partialState);  if (callback) {    this.updater.enqueueCallback(this, callback);  }};复制代码
// src/renderers/shared/reconciler/ReactUpdateQueue.jsenqueueSetState: 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);},复制代码

上述流程图中核心的状态判断,在中

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),};// 批量更新事务的 wrappersvar 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 的作用。

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

简单地说,一个所谓的 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');// 我们自己定义的 Transactionvar 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,关于上文提到的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。

参考原文:

转载于:https://juejin.im/post/5ce53c636fb9a07ebe74977d

你可能感兴趣的文章
Docker入门与实战系列:生态系统
查看>>
.net mysql
查看>>
常用算法之二分查询python&php
查看>>
Securing Web Applications with Apache Shiro(2)
查看>>
boost学习之--shared_ptr
查看>>
python 学习网站
查看>>
Centos7 install python-rrdtoll-1.47 erro
查看>>
npm ERR! code EINTEGRITY 解决方法
查看>>
深受程序员鄙视的外行语录!
查看>>
使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
查看>>
不健康的IT狗,送给你们一句话
查看>>
进程列表中多个JAVA进程的区分识别
查看>>
IPHONE实景导航开发总结
查看>>
Git常用操作命令
查看>>
正则表达式-断言
查看>>
用git合并分支时,如何保持某些文件不被合并
查看>>
局部代码块、构造代码块、静态代码块
查看>>
聚类分析 ---- K-Means算法
查看>>
C語言最新標準-C11 「轉」
查看>>
SaltStack数据系统-Grains详解
查看>>