聊聊setState的合并更新,不是同步异步的问题呐

174 阅读2分钟

一,useState的简易实现

请查看实现简易版useState

二,合并更新 batchedUpdates

2.1 函数组件useState
  • 假设有下面这段代码,点击change按钮后会打印什么
import React, { useState } from "react";
import ReactDOM from "react-dom";
function App() {
  const [count, setCount] = useState(0);
  console.log("render", count);
  const handleBtnClick = () => {
    setCount(1);
    console.log("第一次setCount后", count);
    setCount(2);
    console.log("第二次setCount后", count);
  };
  return (
    <div className="App">
      <button onClick={handleBtnClick}>change</button>
      <span>count:{count}</span>
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById("root"));
  • 这里两次打印的count都是更新前的值0,且只重新render了一次,因为react在此做了性能优化,对两次setCount更新做了合并;

image.png

  • 下面修改下代码,把setCount放在定时器回调里面
import React, { useState } from "react";
import ReactDOM from "react-dom";
function App() {
  const [count, setCount] = useState(0);
  console.log("render", count);
  const handleBtnClick = () => {
    setTimeout(() => {     // +
      setCount(1);
      console.log("第一次setCount后", count);
      setCount(2);
      console.log("第二次setCount后", count);
    });                    // +
  };
  return (
    <div className="App">
      <button onClick={handleBtnClick}>change</button>
      <span>count:{count}</span>
    </div>
  );
}
ReactDOM.render(<App />, document.getElementById("root"));
  • 这里是先打印了render 1再,而且是render了两次,react在此没有按预期做合并setState的操作;

image.png

  • 上面两个不同的结果不是setState同步异步的问题,实际上setState代码都是同步执行的,而且每次执行setState都会调用scheduleUpdateOnFiber尝试触发render。但react把触发setState的回调函数使用了batchedUpdates包裹,这个函数内部会设置executionContext这个全局变量,他在scheduleUpdateOnFiber回调中将控制是否调用flushSyncCallbackQueue来触发render。如果setState放在了setTimeout回调里面,那么在fn(点击事件回调函数handleBtnClick)执行完成后将丢失executionContext上下文,所以此时每次setState都可以触发render,又因为闭包的原因,所以count两次都是打印0;
function batchedUpdates(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  try {
    return fn(a);
  } finally {
    //在执行完回调代码块后,重新修改executionContext并调用flushSyncCallback触发render
    executionContext = prevExecutionContext;
    if (executionContext === NoContext && 
    !( ReactCurrentActQueue$1.isBatchingLegacy)) {
      resetRenderTimer();
      flushSyncCallbacksOnlyInLegacyMode();//会调用performSyncWorkOnRoot
    }
  }
}
  export function scheduleUpdateOnFiber() {
    ...
    const root = markUpdateLaneFromFiberToRoot(fiber, lane);
    if (lane === SyncLane) {
      if (
        (executionContext & LegacyUnbatchedContext) !== NoContext &&
        (executionContext & (RenderContext | CommitContext)) === NoContext
      ) {
        schedulePendingInteractions(root, lane);
        performSyncWorkOnRoot(root);
      } else {
        ensureRootIsScheduled(root, eventTime);
        schedulePendingInteractions(root, lane);
        if (executionContext === NoContext) {
          resetRenderTimer();
          flushSyncCallbackQueue(); //只有当executionContex为NoContext时才会调用flushSyncCallbackQueue进入performSyncWorkOnRoot开始render
        }
      }
      ...
    }
  • 这里如果想要达到期望的在setTimeout中也合并两次setState做批量更新的效果,可以手动调用unstable_batchedUpdates包裹一下回调,这个函数会帮我们加上executionContext上下文
  const handleBtnClick = () => {
    setTimeout(() => {
      unstable_batchedUpdates(() => {  // +
        setCount(1);
        console.log("第一次setCount后", count);
        setCount(2);
        console.log("第二次setCount后", count);
      });                             // +
    });
  };

image.png

2.2 class组件this.setState
  • 修改下代码换成clss组件
class App extends React.Component {
  state = {
    count: 0,
  };
  handleBtnClick = () => {
    const { count } = this.state;
    this.setState({
      count: count + 1,
    });
    console.log('第一次setState后',count);
    this.setState({
      count: count + 1,
    });
    console.log('第二次setState后',count);
  };
  render() {
    const { count } = this.state;
    console.log("render", count);
    return (
      <div className="App">
        <button onClick={this.handleBtnClick}>++</button>
        <span>count:{count}</span>
      </div>
    );
  }
}

image.png

class App extends React.Component {
  state = {
    count: 0,
  };
  handleBtnClick = () => {
    const { count } = this.state;
    setTimeout(() => {        // +
      this.setState({
        count: count + 1,
      });
      console.log("第一次setState后", count);
      this.setState({
        count: count + 1,
      });
      console.log("第二次setState后", count);
    });                       // +
  };
  render() {
    const { count } = this.state;
    console.log("render", count);
    return (
      <div className="App">
        <button onClick={this.handleBtnClick}>++</button>
        <span>count:{count}</span>
      </div>
    );
  }
}

image.png

  • 可以看到和函数组件的情况事一样的,区别在函数组件是调用dispatchAction触发更新,而class组件是调用this.updater.enqueueSetState触发更新;

3 React18的合并更新机制

  • 更新ing