为什么将函数参数传递给 React.useState 而 return 函数将 return 过时值

Why is passing function argument to React.useState and the return function will return stale value

给出下面的代码

import { useState } from "react";
import "./styles.css";

export default function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  const inc1 = () => {
    console.log("debug inc:", count1);
    setCount1((prev) => prev + 1);
  };

  const inc2 = () => {
    console.log("debug inc2:", count2);
    setCount2(count2 + 1);
  };

  const [processInc1] = useState(() => {
    console.log("debug longProcessBeforeInc:", count1);
    // Run some long process here
    return inc1;
  });

  const [processInc2] = useState(() => {
    console.log("debug longProcessBeforeInc:", count2);
    // Run some long process here
    return inc2;
  });

  console.log("debug render:", count1, count2);

  return (
    <div className="App">
      <h3>
        {count1} - {count2}
      </h3>
      <button onClick={inc1}>Inc 1</button>
      <br />
      <br />
      <button onClick={inc2}>Inc 2</button>
      <br />
      <br />
      <button onClick={processInc1}>Long Inc 1</button>
      <br />
      <br />
      <button onClick={processInc2}>Long Inc 2</button>
    </div>
  );
}

inc1inc2processInc1 都按预期工作,您将值增加 1 并正确呈现。

所以与inc1inc2的主要区别是setCount1((prev) => prev + 1);setCount2(count2 + 1);,而processInc1processInc2基本上是return inc1inc2 分别在组件第一次渲染时通过 useState。

我从这里 https://reactjs.org/docs/hooks-faq.html#why-am-i-seeing-stale-props-or-state-inside-my-function 了解到它与闭包有关,但鉴于上面的示例,我无法理解为什么 inc2processInc1 有效但无效processInc2?

这里是上面的link到codesandbox https://codesandbox.io/s/eloquent-morse-37xyoy?file=/src/App.js

你走在正确的道路上。正如你所说,这个问题实际上是由闭包的使用引起的。那么让我们首先向您展示闭包的一个很好的定义:

A closure is the combination of a function and the lexical environment within which that function was declared. This environment consists of any local variables that were in-scope at the time the closure was created.

文档:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

这意味着当您 return 在 useState 中关闭 inc2 时,您还创建了在该特定时间使用的变量的副本(即初始渲染)由那个词法环境(这包括 count2 值)。这就是为什么 processInc2 将保持相同的旧 count2 值。

同时 processInc1 函数将正常工作,因为通过在 useState 中使用回调,您始终可以获得当前count1 状态的值。

最后,inc2 起作用是因为您在单击按钮时直接调用它,因此 count2 值会在那一刻得到评估您调用它(因此它很可能具有当前值)。

这里一个非常重要的细节是 inc1inc2 每次 App 组件渲染时都是 re-defined。

function App() {
  // ...

  const inc1 = () => {
    console.log("debug inc:", count1);
    setCount1((prev) => prev + 1);
  };

  // ...
}

然后将这 2 个函数存储在一个状态中,该状态永远不会改变。

const [processInc1] = useState(() => {
  console.log("debug longProcessBeforeInc:", count1);
  // Run some long process here
  return inc1;
});

这将导致 processInc1processInc2 指向 inc1inc2 的第一个定义(在第一次渲染时创建)。

count1count2 在函数的第一个版本中从不更新的原因是变量从不 re-assigned。这是设计使然。

count1count2 在未来的渲染中改变的唯一原因是因为 useState() 将 return 新值。收到此新值后 inc1inc2 为 re-defined.

然后

processInc1processInc2 被拉出包含 inc1inc2 的第一个定义的 React 状态,因此 count1 的用法这些函数中的 count2 将引用 count1count2.

的第一个值

当你在 inc2 中执行 setCount2(count2 + 1) 并通过 processInc2 调用它时, count2 的值仍然是 0 并且永远不会改变。这是因为 processInc2 指的是 inc2 的第一个定义,而不是当前定义。

setCount1((prev) => prev + 1) 由于不同的函数签名而起作用。其中 inc2 将静态值 (0 + 1) 传递给 setter,inc1 传递一个转换(作为回调)。当您将函数传递给 setCount1 时,React 将使用当前状态作为唯一参数调用该函数。然后 return 值用作新状态。所以尽管 processInc1 仍然使用 inc1 的第一个定义。它总是相关的,因为它描述了必须进行的转换而不是必须设置的值。

请注意,出于上述原因,当通过 processInc1 调用时,console.log("debug inc:", count1); 将继续记录 0