【问题标题】:Missing dependencies in useEffect() really cause stale data?useEffect() 中缺少依赖项真的会导致数据陈旧吗?
【发布时间】:2021-12-22 03:43:46
【问题描述】:

我问这个问题是为了确认我对某些概念的理解。

React 文档强调包含 useEffect() 回调中使用的所有依赖项。如文档中所述:

Otherwise, your code will reference stale values from previous renders.

我有点理解这个解释的出处。但我担心的是“陈旧价值”部分。由于缺少依赖项,我看不到任何可能出现陈旧值的方式。我的论点也得到了文档中的内容的支持:

有经验的 JavaScript 开发人员可能会注意到,传递给 useEffect 的函数在每次渲染时都会有所不同。这是故意的。事实上,这让我们可以从效果内部读取计数值,而不必担心它会过时。

就我的理解而言,如果我们错过了列出依赖项,那么效果将不会在该依赖项更改导致的渲染之后运行,因为 React 认为效果不依赖于它。如果我猜测一下,可能是文档称为引用过时数据的情况。实际上,该数据在效果代码中已过时。但是,效果回调并没有首先运行。在效果运行之前,我不会注意到数据已经过时。如果这很重要,我将首先找出效果没有运行的原因并解决问题。我不会因为数据过时而感到困惑,而是因为效果不会运行。

更重要的是,让我们假设效果在另一个依赖项更改引起的渲染之后运行。在这种情况下,即使我们错过了一个依赖项,由于前面提到的关闭原因,我们也不会读取过时的数据。 I experimented a bit to confirm this:

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

  useEffect(() => {
    console.log(count2);
  }, [count1]);
  
  return (
    <div className="App">
      <div>
        Count1: {count1}
        <button onClick={() => setCount1(count1 + 1)}>increase</button>
      </div>
      <div>
        Count2: {count2}
        <button onClick={() => setCount2(count2 + 1)}>increase</button>
      </div>
    </div>
  );
}

只要效果运行,我们总是会得到最新的 count2。那么我的理解是否成立?

我想知道为什么 React 如此推荐包含所有依赖项。人们通常使用依赖数组来绕过某些效果运行。如果他们省略了依赖项,这很可能是他们想要的。如果是失误,他们很容易注意到效果没有运行并采取行动。

【问题讨论】:

  • 在您的示例中,如果count2 发生更改,则对count1 的任何引用都会过时,您在示例中没有引用count1,所以这不是一个很好的示例。在这里还要清楚一点,它在效果内部是陈旧的,而不是组件。
  • 不完全确定您对什么感到困惑。但这能说明什么吗?即使将 count2 增加到 12,警报也不会触发,除非您在 count2 为 12 时精确更改 count1。useEffect(() =&gt; { if(count2 === 12) { console.log('Count 2 has reached 12, trigger alarm'); } }, [count1]);
  • 啊,您的代码确实表示人们可以响应“为什么警报没有触发?哦,count2 引用回调中的陈旧值”这样的场景。这种反应是体面和自然的。这可能就是为什么文档在这个意义上给出了这样的解释。 @Jashwant

标签: javascript reactjs react-hooks use-effect


【解决方案1】:

我稍微修改了您的示例以显示陈旧的值。

效果通常用于异步原因,因此这并不罕见。

基本上归结为闭包,useEffect 的第一次渲染将在 count1 和 count2 上创建一个闭包,如果效果没有在所有依赖项上重新运行,那么这些闭包将保持(陈旧)。

单击count1 意味着再次调用useEffect,创建setInterval 的新实例,其中包含count1 和count2 的新(非陈旧)副本。因为 count2 不在依赖数组中,所以点击count2 将意味着不会创建新的 setInterval,并且 count1 和 count2 的旧副本会保留在内存中。

公平地说,这可能是 Hooks 的一个难以理解的领域。很容易将 Hook Components 视为对象的一部分,其中包含数据的类。但实际上钩子组件只是渲染函数,useState / useEffect 等,是一种侧面加载到渲染函数管道中。相比之下,React 类组件的数据与对象实例一起存储,因此this.xyz 永远不会过时。

const {useState, useEffect} = React;


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

  useEffect(() => {
    const tm = setInterval(() => {
      console.log(count1, count2);
    }, 1000);
    return () => clearInterval(tm);
  }, [count1]);
  
  return (
    <div className="App">
      <div>
        Count1: {count1}
        <button onClick={() => setCount1(count1 + 1)}>increase</button>
      </div>
      <div>
        Count2: {count2}
        <button onClick={() => setCount2(count2 + 1)}>increase</button>
      </div>
    </div>
  );
}

ReactDOM.render(<App/>,document.querySelector('#mount'));
<script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<div id="mount"></div>

<p>Increase count2, see the console not update until you increase count1,.</p>
<p>Add count2 to the dependancy, and then everything will keep in sync</p>

【讨论】:

  • 您的示例说明了陈旧的数据印象可能如何浮出水面。陈旧的 count2 来自注册到 setInterval 的前一个函数实例。如果 count2 仅在效果回调中同步使用,那么在依赖数组中缺少它不会成为问题。
【解决方案2】:

您的示例代码在一个极其简单的场景中显示了最简单的示例。默认情况下,useEffect 将在每次重新渲染组件时运行。使用依赖数组,内部函数仅在其中一个值更改时运行。我个人遇到过忘记依赖、触发效果以及函数中的值具有陈旧数据的情况。这可能是因为有几个过程同时进行,其中一个更改触发了效果,而另一段数据还没有赶上。切换到使用useReducer 同时控制多个状态位,而不是多个useState,在某些情况下有所帮助,但最终依赖数组使其保持一致。此外(我还没有证实这一点),useEffect 的框架代码可能大量使用了闭包,所以,再次强调,它是为了确保它在正确的过程中引用数据点。

【讨论】:

    猜你喜欢
    • 2021-05-14
    • 2020-03-24
    • 2021-02-23
    • 2021-08-19
    • 2020-10-26
    • 2022-06-12
    • 2019-10-24
    相关资源
    最近更新 更多