详解React setState同+异步机制

React官方文档在描述setState的时候提到了三点注意事项:

  1. 不要直接修改State
  2. State的更新可能是异步的
  3. State的更新会被合并

我们来一一分析为什么有这3点限制。

不要直接修改state

官方文档并没有具体解释不可以直接修改state的原因。只是说明只有构造函数中可以给state直接赋值。如果我们直接修改state,那么组件将不会触发更新。

出于这种设计的原因我理解有这几点:

  1. state的更新意味着组件的重新渲染,通过统一的函数去执行这个操作可以使得整个过程更加可控;因为React并没有像Vue那样采用数据双向绑定的机制,所以它需要一种方式采集到state的变更;
  2. React核心思想是不可变数据,setState内部实际会返回一个新的state,如果直接修改state,可能还是同一个引用;
  3. 由统一的函数去更新state更加利于维护和控制;
  4. 出于性能原因,React可能会合并多次setState的操作为一次;

其实上面几个原因的的核心原因还是React的设计思想是View=F(state)。React通过维护一个不可变的state去渲染视图,更新的时候需要对比新旧state,从而知道最小变更,然后去做更新操作。

这里还涉及到一些性能优化,所以提供一个固定改变state的函数,会让它的整个渲染/更新过程变得更加可控。

State的更新可能是异步的

React的解释是出于性能考虑,React可能会将多次调用合并为一次调用。所以我们不应该期望执行setState后能够理解获取更新后的state。

那么具体什么情况下setState是同步的,什么情况下是异步的呢?

先说结论:通过React生命周期函数以及合成事件执行的setState更新是异步的。而通过类似setTimeout定时器,以及dom.addEventListener等原生JS绑定的事件执行的setState是同步的。

如果我们看setState的源码:

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

enqueueSetState: function(publicInstance, partialState) {
  var internalInstance = getInternalInstanceReadyForUpdate(
    publicInstance,
    'setState',
  );

  var queue =
    internalInstance._pendingStateQueue ||
    (internalInstance._pendingStateQueue = []);
  queue.push(partialState);

  enqueueUpdate(internalInstance);
},

function enqueueUpdate(component) {
  ensureInjected();

  // 这里非常重要,决定其同步更新还是异步更新就是这里判断的
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
复制代码

batchingStrategy.isBatchingUpdates决定了这一次setState的调用是同步更新state还是异步更新。

batchingStrategy.isBatchingUpdates默认是false。这个值的改变是由batchedUpdates函数控制的。batchedUpdates会将isBatchingUpdates改为true。那样setState就是异步更新了。

所以如果我们知道什么时候会调用batchedUpdates,我们也就弄清楚了什么时候setState是同步执行,什么时候是异步执行。

这里需要提到一个事务的概念。当一个事务开始的时候,会调用batchedUpdates将isBatchingUpdates设为true,当事务结束的时候,会将isBatchingUpdates设为false。

React的合成事件、生命周期函数在执行的时候都会生成一个事务,所以在这些情况下执行的setState均表现为异步。

但是如果我们通过类似setTimeout让任务在下一个事件循环中执行,这个时候因为事务已经执行完毕,所以执行setState的时候表现为同步。

通过document.addEventListener执行的事件也是一样的原理。

hooks中state的表现

// 场景1
const [count, setCount] = useState(1);

useEffect(() => {
  setCount(2);
  console.log('count', count); // 1,异步
}, []);


// 场景2
const [count, setCount] = useState(1);

useEffect(() => {
  setTimeout(() => {
    setCount(2);
    console.log("count", count); // 1,异步
  }, 10);
}, []);

// 场景3
const [count, setCount] = useState(1);

const onClick = () => {
  setCount(2);
  console.log("count", count); // 1, 异步
};

return <div onClick={onClick}>{count}</div>;

// 场景4
const [count, setCount] = useState(1);

useEffect(() => {
  document.getElementById("demo").addEventListener("click", () => {
    setCount(2);
    console.log("count", count); // 1, 异步
  });
});

return <div id="demo">{count}</div>;
复制代码

通过以上4个场景,我们可以看到,通过hooks执行的setState,不管是setTimeout又或者是addEventListener这种原生方法,虽然绕过了React事务的过程,但它依然不能立即获取最新的值。究其原因其实我们需要知道函数式组件每次render的实际做了什么。

函数式组件每次render相当于这个函数重新执行了一次,虽然setTimeout是异步执行的,但是因为闭包的原因,它执行的时候拿到的count依然是第一次的值,所以这点它和class组件表现不一样。

State的更新会被合并

这个就比较好理解了,因为state的变更意味着组件需要重新render,为了优化性能,React会收集state的变更,多次调用的情况下,React只会执行一次render。

但如果你按上面说的,在setTimeout中多次执行setState,那么就会导致多次render,所以日常开发过程中,需要避免这种写法。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享