javascript 在React中排队一系列状态更新

new9mtju  于 2023-06-20  发布在  Java
关注(0)|答案(2)|浏览(135)
import { useState } from 'react';

function App() {
  const [number, setNumber] = useState(0);

  function clickHandler() {
    setNumber((n) => {
      console.log('First updater function');
      return n + 1;
    });
    setNumber((n) => {
      console.log('Second updater function');
      return n + 1;
    });
    console.log('Other code in the click handler');
  }

  return (
    <>
      <h1>{number}</h1>
      <button onClick={clickHandler}>Increment number</button>
    </>
  );
}

export default App;

根据本页,https://beta.reactjs.org/learn/queueing-a-series-of-state-updates
传递给状态设置器的函数称为更新器函数。在事件处理程序中的所有其他代码运行完毕后,React将更新程序函数排队处理。在下一次渲染期间,React会遍历队列,并为您提供最终的更新状态。
从上面的陈述中,我理解到,当我单击按钮时,console.log应该按以下顺序出现:
1.单击处理程序中的其他代码
1.第一更新函数
1.第二更新函数
但是,日志将按以下顺序显示:
1.第一更新器函数
1.单击处理程序中的其他代码
1.第二更新器函数
以上顺序仅在我第一次点击按钮时观察到。第二次或多次单击该按钮,顺序再次按预期更改:
1.单击处理程序中的其他代码
1.第一更新器函数
1.第二更新器函数
所以我想知道为什么它第一次点击按钮时表现不同,而且这种行为也不匹配上面React文档页面中所述的?
谢谢你。

bwntbbo3

bwntbbo31#

React中的useState钩子遵循同步状态更新机制。当您在事件处理程序中调用setNumber时,它会立即调用updater function,而不是将其排队等待稍后执行。
这就是为什么当您单击按钮时,您会立即看到来自第一个更新器函数的控制台日志。处理完第一个setNumber后,控件返回到事件处理程序,并记录“Other code in the click handler”,然后处理第二个setNumber调用,因此是来自second updater function的控制台日志。
您注意到的first clicksubsequent clicks之间的行为差异是由于React的批量更新机制。在first click上,由于组件不处于批处理更新阶段,因此每个setNumber调用都被立即单独处理。然而,在随后的点击中,React已经进入了批量更新阶段,因此事件处理程序中的所有状态更新都被批量处理在一起,并在事件结束时立即应用,这与您预期的控制台日志顺序一致。

这是一个简单的演示,演示了如何在JavaScript中实现此机制的简化版本:
let state;  // Current state
let updates = [];  // Queue of updates
let isBatching = false;  // Check if batching is enabled

function useState(initialValue) {
  // Initialize the state
  if (state === undefined) {
    state = initialValue;
  }

  function setState(update) {
    if (isBatching) {
      // If we're in a batching phase, queue the update
      updates.push(update);
    } else {
      // If not, apply the update immediately
      state = update(state);
      render();
    }
  }

  return [state, setState];
}

function flushUpdates() {
  // Apply all updates
  for (let update of updates) {
    state = update(state);
  }
  // Clear the updates queue
  updates = [];
  render();
}

// A simple render function
function render() {
  console.log(`Rendered with state: ${state}`);
}

function clickHandler(setNumber) {
  isBatching = true;
  // Queue two updates
  setNumber((n) => {
    console.log('First updater function');
    return n + 1;
  });
  setNumber((n) => {
    console.log('Second updater function');
    return n + 1;
  });
  console.log('Other code in the click handler');

  // Then flush the updates
  flushUpdates();
  isBatching = false;
}

// Usage:
let [number, setNumber] = useState(0);
clickHandler(setNumber);  // It will log 'First updater function', 'Second updater function', 'Other code in the click handler', 'Rendered with state: 2'

注意事项

作为React的最终用户,你不应该依赖这些实现细节。您的组件不应该依赖于单个事件处理程序中状态更新的确切顺序,因为它可能会在未来版本的React中发生变化。

9cbw7uwe

9cbw7uwe2#

最简单的答案**可能是:
这句话 (目前:“React queues this function to be processed after all other code in the event handler has run”) 确实可能不是100%正确。
React开发人员仍然总是在React上工作,并尽可能地优化事物,考虑不同的边缘情况,有时可能会接受权衡,这使得描述正在发生的事情变得不那么直接。
我可以想象这句话在某些时候是正确的,但在此期间可能已经实现了优化,这使得这句话在某些情况下不正确。
这个细节可能很少相关,但我同意,如果文档做出这样的声明,您应该被允许依赖它。
因此,文档可能应该:

  • 要么加上类似 *"...除了一些优化之外,
  • 或者,如果React认为这是一个实现细节,并且不希望你依赖它,那么它根本不应该提到特定的调用顺序。

分析

“我必须在这里猜测。我发现了一些线索,可以解释发生了什么,但我可能完全错了。另外,React可能在我目前使用的版本18.2.0之后发生了变化。
React只在下一个渲染周期需要结果状态,而不是在当前渲染周期。所以我理解在某些情况下,当没有其他原因需要重新渲染,并且状态没有改变时,React决定跳过下一个渲染周期,作为性能优化。但是 * 要知道 * 状态没有改变,它需要调用updater函数。
请注意,如果第一个更新器函数没有更改状态,那么甚至第二个更新器函数也会在事件处理程序中的其他代码之前被调用(同样仅在第一次调用时):

function clickHandler() {
    setNumber((n) => {
        console.log('First updater function');
        return n + 0;   // <--- state is not changed
    });
    setNumber((n) => {
        console.log('Second updater function');
        return n + 1;
    });
    console.log('Other code in the click handler');
}

控制台输出结果:

First updater function
Second updater function
Other code in the click handler

在React源代码中,在dispatchSetState函数中,有一个注解可能与此相关:

// The queue is currently empty, which means we can eagerly compute the  
// next state before entering the render phase. If the new state is the  
// same as the current state, we may be able to bail out entirely.

相关问题