read-react-source-code icon indicating copy to clipboard operation
read-react-source-code copied to clipboard

请教关于setState的两个问题

Open msforest opened this issue 6 years ago • 6 comments

你好,看完这篇文章,我有两个问题请教一下,还请解答:

  1. setState的异步问题
***
onClick(){
  this.setState(partialState);
  this.setState(partialState);
}
***

每次调用setState时,会调用父类的setState,此时会进入ReactUpdateQueue中,然后会把partialState存放到ReactCompositeComponent实例对象的一个队列中,经过一系列步骤,_processPendingState该方法会对partialState,然后进入到render中。

在这一系列调用过程中,我没有看到哪一步会停止对第一个setState退出当前调用栈,肯定就不会去执行第二个setState,即_processPendingState该方法处理的队列中只有一个partialState,那就谈不上Object.assign会把第二个覆盖第一个state。因为js是单线程,而且底层没有发起异步操作。所以有点疑问

  1. 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.

msforest avatar Nov 25 '18 09:11 msforest

第一个问题,实际上执行enqueueUpdate()之后,setState()就结束了。可以理解成setState()只是记录了一下你要做的改动,并没有真正执行 update。等两个setState()都执行完毕,在当前Transactionclose()里,会去调用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;

我没有再往下追,不过大体来看,这个值应该是用户传入的。

numbbbbb avatar Nov 25 '18 15:11 numbbbbb

你好,第一个问题还是没明白,没看到哪里结束

// 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;

感谢回复

msforest avatar Nov 26 '18 12:11 msforest

我看了下,是这样的,如果没有任何前提,单独调用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。

numbbbbb avatar Nov 26 '18 15:11 numbbbbb

是的,源码里也有comments提到setState处于componentDidMount,这种情况没有问题;我是想到另一种情况

class Test extends Component{
  // ...
  click(){  // 这里click是不在组件的生命周期函数里的
    this.setState(partialState);
    this.setState(partialState);
  }
  render(){
    return <button onclick={this.click}>btn</button>;
  }
}

那我再看看,有新情况我会反馈的

msforest avatar Nov 26 '18 15:11 msforest

我看了一下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

msforest avatar Dec 07 '18 13:12 msforest

好的,我把你的结论补充到内容里:+1:

numbbbbb avatar Dec 07 '18 14:12 numbbbbb