即使延迟调用函数(5 秒)后,函数内的 React 状态也不会改变

use*_*989 1 javascript reactjs react-functional-component use-state

在反应中,我使用功能组件,并且有两个函数(getBooks)和(loadMore)

getBooks从端点获取数据。但是,当我loadMore在函数内单击按钮调用getBooks函数(loadMoreClicked)时,即使在延迟(5秒)调用它之后,它也不会更改它使用以前的状态。但是当我loadMore再次打电话时,状态发生了变化,一切正常。

有人可以解释为什么初始调用 (getBooks) 时的 (loadMoreClicked) 即使在 5 秒延迟后调用它也没有更新。

function component() {
  const [loadMoreClicked, setLoadMore] = useState(false);
  const getBooks = () => {
    const endPoint = `http://localhost/getBooks`; //this is my end point
    axios
      .get(endPoint, {
        params: newFilters
      })
      .then(res => {
        console.log(loadMoreClicked); //the (loadMoreClicked) value is still (false) after (5 sec)
      })
      .catch(err => {
        console.log(err);
      });
  };

  const loadMore = () => {
    setLoadMore(true); //here i am changing (loadMoreClicked) value to (true)

    setTimeout(() => {
      getBooks(); // i am calling (getBooks()) after 5 seconds.
    }, 5000);
  };

  return (
    <div>
      <button onClick={() => loadMore()}>loadMore</button> //calling (loadMore)
      function
    </div>
  );
}
Run Code Online (Sandbox Code Playgroud)

Mic*_*dis 6

有两件事正在发生:

  1. getBooks()使用const周围函数中定义的值。当函数引用constlet定义之外的变量时,它会创建所谓的闭包。闭包从这些外部变量中获取值,并为内部函数提供这些值的副本,就像构建函数时一样。在本例中,该函数是在最初调用状态后立即构建的,并loadMoreClicked设置为false

  2. 那么为什么不setLoadMore(true)触发重新渲染并重写函数呢?当我们设置状态时,重新渲染不会立即发生。它被添加到 React 管理的队列中。这意味着,loadMore()执行时setLoadMore(true)会说“在运行完其余代码后更新状态”。重新渲染发生在函数结束之后,因此getBooks()使用的副本是在此周期中构建并排队的副本,其中包含内置的原始值。

对于您正在做的事情,您可能希望在超时中调用不同的函数,具体取决于是否单击了按钮。或者,您可以根据是否要getBooks()考虑单击的按钮来创建另一个更立即的关闭,如下所示:

const getBooks = wasClicked => // Now calling getBooks(boolean) returns the following function, with wasClicked frozen
  () => {
    const endPoint = `http://localhost/getBooks`;
    axios
    .get(endPoint, {
      params: newFilters
    })
    .then(res => {
      console.log(wasClicked); // This references the value copied when the inner function was created by calling getBooks()
    })
    .catch(err => {
      console.log(err);
    });
  }

...

const loadMore = () => {
  setLoadMore(true);
  setTimeout(
    getBooks(true), // Calling getBooks(true) returns the inner function, with wasClicked frozen to true for this instance of the function
    5000
  );
};
Run Code Online (Sandbox Code Playgroud)

还有第三种选择,即重写const [loadMoreClicked, setLoadMore]var [loadMoreClicked, setLoadMore]. 虽然引用const变量会冻结该时刻的值,var但不会。var允许函数动态引用变量,以便该值在函数执行时确定,而不是在定义函数时确定。

这听起来像是一个快速而简单的修复,但是当在诸如上面的第二个解决方案之类的闭包中使用时,它可能会导致混乱。在这种情况下,由于闭包的工作原理,该值再次固定。因此,您的代码会将值冻结在闭包中,但不会冻结在常规函数中,这可能会导致更多混乱。

我个人的建议是保留const定义。var开发社区较少使用它,因为它在闭包和标准函数中的工作方式令人困惑。在实践中,大多数(如果不是全部)钩子都会填充常量。将其作为单独的var参考会使未来的开发人员感到困惑,他们可能会认为这是一个错误并更改它以适应模式,从而破坏您的代码。

如果您确实想动态引用 的状态loadMoreClicked,并且不一定需要重新渲染组件,我实际上建议使用useRef()而不是useState().

useRef创建一个具有单个属性 的对象current,该属性保存您放入其中的任何值。当您更改 时current,您正在更新可变对象上的值。因此,即使对对象的引用被及时冻结,它仍然引用具有最新值的可用对象。

这看起来像:

function component() {
  const loadMoreClicked = useRef(false);
  const getBooks = () => {
    const endPoint = `http://localhost/getBooks`;
    axios
    .get(endPoint, {
      params: newFilters
    })
    .then(res => {
      console.log(loadMoreClicked.current); // This references the property as it is currently defined
    })
    .catch(err => {
      console.log(err);
    });
 }


  const loadMore = () => {
    loadMoreClicked.current = true; // property is uodated immediately
    setTimeout(getBooks(), 5000);
  };

}
Run Code Online (Sandbox Code Playgroud)

这是有效的,因为,虽然loadMoreClickedconst在顶部被定义为 a ,它是对对象的常量引用,而不是常量值。被引用的对象可以根据需要进行更改。

这是 Javascript 中最令人困惑的事情之一,并且它通常在教程中被掩盖,因此除非您有一些使用指针(例如 C 或 C++)的后端经验,否则这会很奇怪。

因此,对于您正在做的事情,我建议使用 useRef() 而不是 useState()。如果您确实想重新渲染组件,例如,如果您想在加载内容时禁用按钮,然后在加载内容时重新启用它,我可能会同时使用两者,并将它们重命名为更清楚地说明其用途:

function component() {
  const isLoadPending = useRef(false);
  const [isLoadButtonDisabled, setLoadButtonDisabled] = useState(false);
  const getBooks = () => {
    const endPoint = `http://localhost/getBooks`;
    axios
    .get(endPoint, {
      params: newFilters
    })
    .then(res => {
      if (isLoadPending.current) {
        isLoadPending.current = false:
        setLoadButtonDisabled(false);
      }
    })
    .catch(err => {
      console.log(err);
    });
 };

  const loadMore = () => {
    isLoadPending.current = true;
    setLoadButtonDisabled(true);
    setTimeout(getBooks(), 5000);
  };

}
Run Code Online (Sandbox Code Playgroud)

它有点冗长,但它有效,并且可以分离您的关注点。ref 是你的标志,告诉你的组件它现在正在做什么。状态指示组件应如何呈现以反映按钮。

设置状态是一种即发即忘的操作。在组件的整个函数执行完毕之前,您实际上不会看到其中的变化。请记住,您必须先获得值,然后才能使用 setter 函数。因此,当您设置状态时,您不会在此周期中更改任何内容,而是告诉 React 运行另一个周期。它足够聪明,不会在第二个周期完成之前渲染任何内容,因此速度很快,但它仍然从上到下运行两个完整的周期。