内容简介:问上述代码中 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源码分析
- 如何阅读源码?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
垃圾回收的算法与实现
中村成洋、相川光 / 丁灵 / 人民邮电出版社 / 2016-7-1 / 99.00元
★ Ruby之父Matz作推荐语:上古传承的魔法,彻底揭开垃圾回收的秘密! ★ 日本天才程序员兼Lisp黑客竹内郁雄审校 本书前半介绍基本GC算法,包括标记-清除GC、引用计数、复制算法的GC、串行GC的算法、并发GC的算法等。后半介绍V8、Rubinius、Dalvik、CPython等几种具体GC的实现。本书适合各领域程序员阅读。一起来看看 《垃圾回收的算法与实现》 这本书的介绍吧!