read-react-source-code
read-react-source-code copied to clipboard
请教关于setState的两个问题
你好,看完这篇文章,我有两个问题请教一下,还请解答:
- setState的异步问题
***
onClick(){
this.setState(partialState);
this.setState(partialState);
}
***
每次调用setState
时,会调用父类的setState
,此时会进入ReactUpdateQueue
中,然后会把partialState
存放到ReactCompositeComponent
实例对象的一个队列中,经过一系列步骤,_processPendingState
该方法会对partialState
,然后进入到render
中。
在这一系列调用过程中,我没有看到哪一步会停止对第一个setState退出当前调用栈,肯定就不会去执行第二个setState,即_processPendingState
该方法处理的队列中只有一个partialState
,那就谈不上Object.assign
会把第二个覆盖第一个state。因为js是单线程,而且底层没有发起异步操作。所以有点疑问
- setState里面的updater
function ReactComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
updater是由ReactCompositeComponent调用new Component时传入的第三个参数
var inst = new Component(publicProps, publicContext, updateQueue);
然后像我们一般定义组件是这样的
class Test extends React.Component{
constructor(props){
super(props); // 没有传入第二三个参数
}
}
写这样的代码,并没有传入第三个参数,为何updater还能用的这么好?
thanks in advance.
第一个问题,实际上执行enqueueUpdate()
之后,setState()
就结束了。可以理解成setState()
只是记录了一下你要做的改动,并没有真正执行 update。等两个setState()
都执行完毕,在当前Transaction
的close()
里,会去调用flushBatchedUpdates()
,此时才真正去做 update。
代码里的isBatchingUpdates
会控制,一般来说这个值都是true
,这样就只做 enqueue,不去 update。如果是false
,那就是你说的情况,setState()
会把操作做完,真正的 update 完,然后执行下一个setState()
。
第二个问题,实际上new Component()
是 React 的内部方法,对于用户来说你只能传入 props 这一个参数,后面两个都是 React 自己控制的。
默认的updater
会在具体的文件中自动被注入,React 有几种不同的场景,比如 server render,(新一代的)fiber,还有我们默认用的 stack。比如 stack,对应的文件是/src/renderers/shared/stack/reconciler/ReactUpdateQueue.js
,里面定义了ReactUpdateQueue
。搜索一下可以发现,在/src/renderers/dom/client/ReactReconcileTransaction.js
有一处:
getUpdateQueue: function() {
return ReactUpdateQueue;
},
继续搜索getUpdateQueue
,发现/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js
有:
var updateQueue = transaction.getUpdateQueue();
这行代码在函数mountComponent()
内部。
在这个文件搜索updateQueue
,会看到:
var inst = this._constructComponent(
doConstruct,
publicProps,
publicContext,
updateQueue,
);
下面还有一处也用到了updateQueue
,赋值给inst
。
因此可以看到,updateQueue
的值其实是 React 自己去设置的,和用户没关系。往回看一下publicProps
,这个值来自:
var publicProps = this._currentElement.props;
我没有再往下追,不过大体来看,这个值应该是用户传入的。
你好,第一个问题还是没明白,没看到哪里结束
// ReactBaseClasses.js
ReactComponent.prototype.setState = function(){
this.updater.enqueueSetState(this, partialState);
}
// ReactUpdateQueue.js
enqueueSetState: function(publicInstance, partialState) {
// 得到的是ReactCompositeComponent的实例对象
var internalInstance = getInternalInstanceReadyForUpdate(
publicInstance,
'setState',
);
if (!internalInstance) {
return; // 这里不会被执行到
}
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance); // 进入该方法
}
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
// ReactUpdate.js
function enqueueUpdate(component) {
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component); // 这里会执行,进入batchdUpdates
return;
}
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
// ReactDefaultBatchingStrategy.js
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
// The code is written this way to avoid extra allocations
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
return transaction.perform(callback, null, a, b, c, d, e); // 这里在transaction处理enqueueUpdate
// ReactDefaultBatchingStrategyTransaction这个事物在close阶段会处理ReactUpdates.flushBatchedUpdates
}
}
//
var flushBatchedUpdates = function() {
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
}
}
var runBatchedUpdates = function(){
//...
ReactReconciler.performUpdateIfNecessary( // 这里会进入到virtual DOM更新
component,
transaction.reconcileTransaction,
updateBatchNumber,
);
//...
}
// 到此为止,我都还没看到哪里结束setState调用栈了,还请指出哪里理解有误,谢谢
第二个问题:
后面还有一行赋值操作
inst.updater = updateQueue;
感谢回复
我看了下,是这样的,如果没有任何前提,单独调用setState()
确实和你说的一样,会进入 transaction 然后执行所有代码。但是一般来说,setState()
的执行都是处在其他 transaction 中,比如文章里说的render()
过程中的componentDidMount()
,实际上在调用setState()
之前就已经在 batching 了,所以enqueueUpdate()
的第一个 if 并没有执行,也就是说没有继续进入 transaction,直接执行 push 和计数然后返回了。
React 的 hook 一般都在 transaction 内部,所以这些函数里的setState()
都是这个模式。Event Handler 里我还没有仔细看,你可以看下,我猜应该也会在某个地方进入一个 transaction,否则 handler 里多次调用setState()
就会像你说的一样直接进行 update。如果你找到了可以和我说下,我加到内容里。
第二个问题,确实还有一个,你看两个赋值中间有对inst
进行操作和处理,具体做了什么可以跟踪代码看下。所以这俩赋值没啥冲突,大概是中间改变了inst
,这里又加了一遍确保有 updater。
是的,源码里也有comments提到setState
处于componentDidMount
,这种情况没有问题;我是想到另一种情况
class Test extends Component{
// ...
click(){ // 这里click是不在组件的生命周期函数里的
this.setState(partialState);
this.setState(partialState);
}
render(){
return <button onclick={this.click}>btn</button>;
}
}
那我再看看,有新情况我会反馈的
我看了一下react的事件,它是把所有事件都注册到了document上,利用事件冒泡/捕获的特性,对事件进行了一层处理,也就是我们理解的合成事件SyntheticEvent
,当我们人为调用setState
(例如:事件函数、生命周期函数等),会先触发在document上的事件
var ReactEventListener = {
dispatchEvent: function (topLevelType, nativeEvent) {
...
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
...
}
}
打开batchedUpdates
的开关,即ReactDefaultBatchingStrategy.isBatchingUpdates = true;
。
然后会从EventPluginHub
获取相应的回调函数,这时候,这些callback就都处于正在处于transaction
中了;
如果不是人为触发的setState
(例如:setTimeout),这种情况就是一开始我理解的--所有的setState都是独立的transaction
好的,我把你的结论补充到内容里:+1: