搜索

首页  >  问答  >  正文

React18微任务批处理问题

useEffect(() => {
  console.log("render");
});

const handleClick = () => {
  setC1((c) => c + 1);

  Promise.resolve().then(() => {
    setC1((c) => c + 1);
  });
};

const handleClick2 = () => {
  Promise.resolve().then(() => {
    setC1((c) => c + 1);
  });

  setC1((c) => c + 1);
};

在React18版本中,为什么点击handleClick方法会出现两次渲染,而点击handleClick2方法只会出现一次渲染?

我希望这两种方法的输出是相同的。谁能告诉我为什么它们不同?

P粉506963842P粉506963842448 天前507

全部回复(1)我来回复

  • P粉642920522

    P粉6429205222023-09-08 17:06:21

    我将解释这些调用顺序有何不同,以及观察到的行为如何可能。

    我无法确切地告诉你 React 内部是如何批量更新状态的, 我只是假设 React 进行了复杂的优化,这与使用 React 的开发人员无关,并且需要深入了解 React 内部,甚至可能从一个版本更改为另一个版本。 (请随时纠正我。)

    区别

    Promise.resolve() 安排一个新的微任务,实际上相当于 window.queueMicrotask()

    setState 函数(可能)还会安排一个新的微任务, 因此它们的回调(PromisesetState)都是在同一执行阶段调用的。

    这两个变体的区别在于

    • handleClickA 中,在两个 updater 函数之间调用 setState2 挂钩,而
    • handleClickB 中,两个 updater 函数都会直接依次调用。

    示例代码

    我稍微重写了您的代码,以更好地说明调用顺序:

    const setState1 = setState;     
    const setState2 = setState;
    const update1 = updaterFunction; // c => c + 1
    const update2 = updaterFunction; // c => c + 1
    
    const handleClickA = () => {          
                                      // Scheduled functions:
        setState1( update1 );         // 1. --> [ update1 ]
        
        queueMicrotask(() => {        // 2. --> [ update1, setState2 ]
            setState2( update2 );     // 4. --> [ update2 ]
        });
    
        // update1();                 // 3. --> [ setState2 ]
        // setState2( update2 );      // 4. --> [ update2 ]
        // update2();                 // 5. --> []
    };
    
    const handleClickB = () => {
                                      // Scheduled functions:    
        queueMicrotask(() => {        // 1. --> [ setState2 ]
            setState2( update2 );     // 3. --> [ update2 ]
        });
    
        setState1( update1 );         // 2. --> [ setState2, update1 ]
        
        // setState2( update2 );      // 3. --> [ update1, update2 ]
        // update1();                 // 4. --> [ update2 ]
        // update2();                 // 5. --> []
    };

    呼叫顺序说明

    这里我说明了调用顺序。

    (FIFO >):

    handleClickA

    // 0. --> []
    - schedule update1 (setState1())  // 1. --> [ update1 ]
    - schedule setState2              // 2. --> [ update1, setState2 ]
    - invoke update1()                // 3. --> [ setState2 ]
    - schedule update2 (setState2())  // 4. --> [ update2 ]
    - invoke update2()                // 5. --> []

    handleClickB

    // 0. --> []
    schedule setState2              // 1. --> [ setState2 ]
    schedule update1 (setState1())  // 2. --> [ setState2, update1 ]
    schedule update2 (setState2())  // 3. --> [ update1, update2 ]
    invoke update1()                // 4. --> [ update2 ]
    invoke update2()                // 5. --> []

    个人解读

    我假设 React 尝试对当前排队的所有 updater 函数进行批处理。

    即只要仅调用更新器函数,请尝试将它们批处理在一起仅更新一次最终状态。

    但是,如果调用了新的 setState 函数,React 可能会完成当前更新循环,并在调用下一个 updater 之前启动新的渲染周期 函数。

    我只能猜测为什么要这样做

    • 因为新的 setState 可能会以某种方式破坏批处理,或者
    • 如果递归调用新的 setState 调用,下一次渲染将会延迟太多,或者
    • React 人员仍在研究最佳优化策略及其权衡。
    • (...或者这是一个错误。)

    回复
    0
  • 取消回复