内容简介:问上述代码中 4 次 console.log 打印出来的 val 分别是多少? 不卖关子,先揭晓答案,4 次 log 的值分别是:0、0、2、3。 若结果和你心中的答案不完全相同,那下面的内容你可能会感兴趣。 同样的 setState 调用,为何表现和结果却大相径庭呢?让我们先看看 setState 到底干了什么。所以基于上述结论,如果想要实现上述代码中 4 次 console.log 打印出来的 val 分别是1、2、3、4。可以实现如下:或者
举个:chestnut:
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); // 1 this.setState((prevState) => { return { count: prevState.val + 1 } }) console.log(this.state.val); // 2 this.setState((prevState) => { return { count: prevState.val + 1 } }) console.log(this.state.val); // 3 this.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.js ReactComponent.prototype.setState = function(partialState, callback) { // ... this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback); } }; 复制代码
// src/renderers/shared/reconciler/ReactUpdateQueue.js enqueueSetState: 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); }, 复制代码
上述流程图中核心的状态判断,在 源码(ReactUpdates.js) 中
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), }; // 批量更新事务的 wrappers var 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 的作用。
/* * <pre> * wrappers (injected at creation time) * + + * | | * +-----------------|--------|--------------+ * | v | | * | +---------------+ | | * | +--| wrapper1 |---|----+ | * | | +---------------+ v | | * | | +-------------+ | | * | | +----| wrapper2 |--------+ | * | | | +-------------+ | | | * | | | | | | * | v v v v | wrapper * | +---+ +---+ +---------+ +---+ +---+ | invariants * perform(anyMethod) | | | | | | | | | | | | maintained * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|--------> * | | | | | | | | | | | | * | | | | | | | | | | | | * | | | | | | | | | | | | * | +---+ +---+ +---------+ +---+ +---+ | * | initialize close | * +-----------------------------------------+ * </pre> */ 复制代码
简单地说,一个所谓的 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'); // 我们自己定义的 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 复制代码
当然在实际代码中 React 还做了异常处理等工作,这里不详细展开。有兴趣的同学可以参考源码中 Transaction 实现。
说了这么多 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。
参考原文:
以上所述就是小编给大家介绍的《React setState源码阅读》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【源码阅读】AndPermission源码阅读
- 【源码阅读】Gson源码阅读
- 如何阅读Java源码 ,阅读java的真实体会
- 我的源码阅读之路:redux源码剖析
- JDK源码阅读(六):HashMap源码分析
- 如何阅读源码?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Visual C#从入门到精通(第8版)
夏普 (John Sharp) / 周靖 / 清华大学出版社 / 2016-6-1
《Visual C#从入门到精通(第8版)》共27章,结构清晰,叙述清楚。所有练习均在Visual Studio 2015简体中文版上进行过全面演练。无论是刚开始接触面向对象编程的新手,还是打算迁移到C#的C、C++或Java程序员,都可以从《Visual C#从入门到精通(第8版)》汲取到新的知识。迅速掌握C#编程技术。一起来看看 《Visual C#从入门到精通(第8版)》 这本书的介绍吧!