「React」中 setState 异步 or 同步 ❓

182 阅读2分钟

这是一道 React 送命题,考察面试者对 React 运行机制的理解程度。

🔢 经典原题

class Test extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }
    
  componentDidMount() {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // => 0
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // => 0
    setTimeout(() => {
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count); // => 1 + 1 = 2
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count); // => 2 + 1 = 3
    }, 0)
  }
}

💯 输出结果

0 0 2 3

❓ 参考解析

React 为了优化渲染性能,通过 isBatchingUpdates 实现合并操作并延迟更新 UI state 的策略。但是具体 setState 是同步更新还是异步更新,还需要结合具体场景判断。

异步更新:React 可以控制的区域(生命周期 componentDidiMount ...,合成事件 onClick、onChange...)

componentDidMount() {
  // 异步状态更新
}

同步更新:React API 以外的原生操作(addEventListener、setTimeout、setInterval)

setTimeout(() => {
  // 同步状态更新
}, 0)

📑 源码解读

setState 继承至 React.Component,会调用 enqueueSetState

ReactComponent.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState'); 📌
  }
};

enqueueSetState 函数将状态加入待更新状态队列:

enqueueSetState: function (publicInstance, partialState) {
  // 根据 this 拿到对应的组件实例
  var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
  // 这个 queue 对应的就是一个组件实例的 state 数组
  var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
  queue.push(partialState);
  //  enqueueUpdate 用来处理当前的组件实例
  enqueueUpdate(internalInstance); 📌
}

enqueueUpdate 函数将组件实例加入待更新组件队列:

function enqueueUpdate(component) {
  ensureInjected();
  // 注意这一句是问题的关键,isBatchingUpdates标识着当前是否处于批量创建/更新组件的阶段
  if (!batchingStrategy.isBatchingUpdates) { 📌
    // 若当前没有处于批量创建/更新组件的阶段,则立即更新组件
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 否则,先把组件塞入 dirtyComponents 队列里,让它“再等等”
  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

isBatchingUpdates 表示是否处于更新状态:

var ReactDefaultBatchingStrategy = {
  // 全局唯一的锁标识
  isBatchingUpdates: false, 📌
 
  // 发起更新动作的方法
  batchedUpdates: function(callback, a, b, c, d, e) {
    // 缓存锁变量
    var alreadyBatchingStrategy = ReactDefaultBatchingStrategy. isBatchingUpdates
    // 把锁“锁上”
    ReactDefaultBatchingStrategy. isBatchingUpdates = true

    if (alreadyBatchingStrategy) {
      callback(a, b, c, d, e)
    } else {
      // 启动事务,将 callback 放进事务里执行
      transaction.perform(callback, null, a, b, c, d, e) 📌
    }
  }
}

委托 Transaction 事务进行更新操作:

 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |   v     v                     v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | +---+ +---+   +---------+   +---+ +---+ |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+

形成的开关机制,实现 state 收集,批量合并更新操作,表现为异步:

add = () => {
  // 进来先锁上
  isBatchingUpdates = true 📌
  console.log('add前', this.state.num)
  this.setState({
    num: this.state.num + 1
  });
  console.log('add后', this.state.num)
  // 执行完函数再放开
  isBatchingUpdates = false 📌
}

由于 setTimeout 为异步的宏任务,当回调执行时,实际上 isBatchingUpdates 已经为 false,

因此在效果上,表现为同步:

reduce = () => {
  // 进来先锁上
  isBatchingUpdates = true 📌
  setTimeout(() => {
    console.log('reduce前的', this.state.num)
    this.setState({
      num: this.state.num - 1
    });
    console.log('reduce后的', this.state.num)
  },0);
  // 执行完函数再放开
  isBatchingUpdates = false 📌
}

📊 流程图

sturt.png