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

React state inside a function is not changing even after calling it with a delay of (5 seconds)

在 React 中,我使用的是功能组件,我有两个功能 (getBooks) 和 (loadMore)

getBooks 从端点获取数据。但是,当我在 getBooks 函数 (loadMoreClicked) 内单击按钮调用 loadMore 函数时,它没有更改,即使在延迟(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>
  );
}

您可以使用 useEffect 方法来监视 loadMoreClicked 更新,例如 componentDidUpdate 生命周期方法并在其中调用 setTimeout

useEffect(() => {
    if(loadMoreClicked){
      setTimeout(() => {
        getBooks();
      }, 5000);
    }
  }, [loadMoreClicked])

只有在loadMoreClicked改为true后,我们才调用setTimeout

这归结为闭包在 JavaScript 中的工作方式。赋予 setTimeout 的函数将从初始渲染中获取 loadMoreClicked 变量,因为 loadMoreClicked 没有发生变化。

发生了两件事:

  1. getBooks() 正在使用周围函数中定义的 const 值。当函数在其定义之外引用 constlet 变量时,它会创建所谓的 闭包 。闭包从那些外部变量中获取值,并将这些值的内部函数副本提供为 它们在构建函数时的状态。 在这种情况下,函数是在状态被构建之后立即构建的最初被调用,loadMoreClicked 设置为 false.

  2. 那么为什么 setLoadMore(true) 没有触发重新渲染并重写函数呢?当我们设置状态时,重新渲染不会立即发生。它被添加到 React 管理的队列中。这意味着,当执行 loadMore() 时,setLoadMore(true) 表示 "update the state after I'm done running the rest of the code." 重新渲染发生在函数结束之后,因此使用的 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
  );
};

还有第三种选择,就是把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);
  };

}

这是有效的,因为虽然 loadMoreClicked 在顶部定义为 const,但它是一个常量 对对象的引用, 不是常量值。被引用的对象可以随心所欲地改变。

这是 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);
  };

}

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

设置状态是一种即发即弃的操作。在执行组件的整个功能之前,您实际上不会看到其中的变化。请记住,您在使用 setter 函数之前获得了您的值。所以当你设置状态时,你并没有改变这个循环中的任何东西,你告诉 React 运行 另一个循环。它足够聪明,不会在第二个循环完成之前不渲染任何东西,所以速度很快,但它仍然 运行 两个完整的循环,从上到下。