Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React - setState源码分析 #29

Open
amandakelake opened this issue Mar 9, 2018 · 3 comments
Open

React - setState源码分析 #29

amandakelake opened this issue Mar 9, 2018 · 3 comments
Labels

Comments

@amandakelake
Copy link
Owner

amandakelake commented Mar 9, 2018

请先看官方文档

上来先看官方文档中对setState()的定义
英文文档最佳
英文-React.Component - React
中文-React.Component - 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>
    );
  }
}

2018-03-09 11 36 19

为什么会只加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);

2018-03-09 11 46 01

二、setState源码世界

相信能到这里的同学都知道了setState()是个既能同步又能异步的方法了,那具体什么时候是同步的,什么时候是异步的?只有去源码里面看实现是最靠谱的方式。

注:这里说的同步和异步只是“实现上看起来像同步还是异步,比如上面答案二setTimeout里面,看起来就是同步的”,实质上setState()是异步的

不管这里看不看得懂都没关系了,马上进入源码的世界。

1、如何快速查看react源码

上react的github仓库,直接clone下来
GitHub - facebook/react: A declarative, efficient, and flexible JavaScript library for building user interfaces.

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

到目前我看为止,最新的版本是16.2.0,我选了15.6.0的代码
一是为了参考前辈们的分析成果
二来,我水平有限,如果写的实在不清晰,同学们还可以参考着其他人的分析文章一起读,而不至于完全理解不了

如何切换版本?
1、找到对应版本号
05c0339f-9d91-46ce-b63b-45a8b2d5cadd

2、复制15.6.0的历史记录号
5b8ff072-c541-443a-b16d-18761561ec6d

3、回滚

git reset --hard 911603b

如图,成功回滚到15.6.0版本

6d337778-23fc-46a4-8116-d15d83871ae7

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

4382ff01-1583-4f72-a15e-1423b5ca7407

这个文件导出了一个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
2250445a-5818-4c4e-9708-ad0e3078eaca

找到enqueueUpdate方法
5cd44362-f139-49d9-b8cd-aa30404d7bc4

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中一图
978d08b6-b6b3-46bd-aefe-3ec18fdfab1f

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
99b54f04-708b-4e16-aaa4-f932ad4782ed

简单地说,一个所谓的 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);

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

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

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

在事务结束的时候才通过 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。
7f6d533a-fcd4-4b20-a4a2-89abbba4bc9f

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,就相当于同步更新的过程了。

参考
React技术内幕 setState的秘密 - 掘金
React源码分析 - 组件更新与事务 - 掘金
源码看React setState漫谈(一) - 前端成长之路 - SegmentFault 思否
源码看React setState漫谈(二) - 前端成长之路 - SegmentFault 思否

@ychow
Copy link

ychow commented Apr 12, 2018

切换版本,在branch里面有tag.

@yangfan0095
Copy link

👍

@zhaoleipeng
Copy link

用定时器的方式获取真的还会执行所以他们会触发两次批量更新batchedUpdates,也就会执行两个事务和函数flushBatchedUpdates么?我看过另一个BLOG中统计生命周期貌似木有经过batchedUpdates这个阶段。也不知道是不是对的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants