在 useRef() 中存储回调

2024-05-12

这是一个可变引用的示例,它存储来自反应过度的博客 https://overreacted.io/making-setinterval-declarative-with-react-hooks/:

function useInterval(callback, delay) {
  const savedCallback = useRef();

  // update ref before 2nd effect
  useEffect(() => {
    savedCallback.current = callback; // save the callback in a mutable ref
  });

  useEffect(() => {
    function tick() {
      // can always access the most recent callback value without callback dep 
      savedCallback.current(); 
    }

    let id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

然而,React Hook FAQ 指出该模式是不建议 https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback:

另请注意,此模式可能会导致问题并发模式 https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html. [...]

无论哪种情况,我们都不推荐这种模式,只是为了完整性而在此处显示它。

我发现这种模式非常有用,特别是对于回调来说,并且不明白为什么它在常见问题解答中得到了红旗。例如,客户端组件可以使用useInterval无需包裹useCallback围绕回调(更简单的 API)。

另外,在并发模式下应该不会有问题,因为我们更新了里面的引用useEffect。从我的角度来看,常见问题解答条目可能有错误的观点(或者我误解了它)。

所以,总而言之:

  1. 有什么从根本上反对在可变引用中存储回调吗?
  2. 像上面的代码一样,在并发模式下安全吗?如果不安全,为什么不呢?

小免责声明:我不是核心 React 开发人员,也没有看过 React 代码,所以这个答案是基于阅读文档(字里行间)、经验和实验

Also 这个问题 https://stackoverflow.com/questions/68407187/potential-bug-in-official-useinterval-example自从明确指出了意外行为以来一直被问到useInterval()执行

有什么从根本上反对在可变引用中存储回调吗?

我对反应文档的阅读是,不建议这样做,但在某些情况下可能仍然是有用的甚至是必要的解决方案,因此“逃生舱口”参考,所以我认为答案是“不”。我认为不推荐,因为:

  • 您对所保存的闭包的生命周期的管理拥有明确的所有权。当它过时时,你需要自己修复它。

  • 这很容易以微妙的方式出错,见下文。

  • 文档中给出了此模式,作为如何在处理程序更改时重复渲染子组件的示例,并且作为文档说 https://reactjs.org/docs/hooks-faq.html#how-to-read-an-often-changing-value-from-usecallback:

    最好避免深入传递回调

    例如使用上下文。这样,每次重新渲染父母时,您的孩子就不太可能需要重新渲染。因此,在这个用例中,有一种更好的方法来做到这一点,但这将依赖于能够更改子组件。

但是,我确实认为这样做可以解决某些其他方式难以解决的问题,并且拥有像这样的库函数的好处useInterval()在您的代码库中经过测试和现场强化,其他开发人员可以使用,而不是尝试使用自己的代码setInterval直接(可能使用全局变量......这会更糟糕)将超过使用的负面影响useRef()来实施它。如果存在错误,或者通过更新引入错误来做出反应,则只有一个地方可以修复它。

另外,无论如何,您的回调在过期时可能是安全的,因为它可能只是捕获了不变的变量。例如,setState返回的函数useState()保证不会改变,请参阅最后一条注释this https://reactjs.org/docs/hooks-reference.html#usestate,所以只要你的回调只使用这样的变量,你就可以坐得很好。

话虽如此,实施setInterval()您给出的确实有一个缺陷,请参阅下文以及我建议的替代方案。

当像上面的代码那样完成时,在并发模式下安全吗(如果不安全,为什么)?

现在我不完全知道并发模式是如何工作的(据我所知,它还没有最终确定),但我的猜测是并发模式很可能会加剧下面的窗口条件,因为据我所知,它可能会将状态更新与渲染分开,增加了仅在以下情况下更新的回调的窗口条件:useEffect()当它过期时,将调用 fires (即渲染时)。

示例表明您的useInterval过期时可能会弹出。

在下面的例子中我证明了setInterval()计时器可能会弹出between setState()以及调用useEffect()它设置更新的回调,这意味着回调在过期时被调用,如上所述,这可能没问题,但可能会导致错误。

在示例中我修改了你的setInterval()这样它会在发生一些事件后终止,并且我使用了另一个引用来保存“真实”值num。我用两个setInterval()s:

  • 只需记录以下值num存储在 ref 和渲染函数局部变量中。
  • 其他定期更新num,同时更新中的值numRef并打电话setNum()导致重新渲染并更新局部变量。

现在,如果保证在调用时setNum() the useEffect()下一个渲染的 s 将立即被调用,我们希望立即安装新的回调,因此不可能调用过时的闭包。然而我的浏览器中的输出是这样的:

[Log] interval pop 0 0 (main.chunk.js, line 62)
[Log] interval pop 0 1 (main.chunk.js, line 62, x2)
[Log] interval pop 1 1 (main.chunk.js, line 62, x3)
[Log] interval pop 2 2 (main.chunk.js, line 62, x2)
[Log] interval pop 3 3 (main.chunk.js, line 62, x2)
[Log] interval pop 3 4 (main.chunk.js, line 62)
[Log] interval pop 4 4 (main.chunk.js, line 62, x2)

每次数字都不同,说明回调已在setNum()已被调用,但在第一个回调配置之前useEffect().

添加更多跟踪后,差异日志的顺序显示为:

  1. setNum()叫做,
  2. render() occurs
  3. “间隔弹出”日志
  4. useEffect()更新 ref 被调用。

IE。计时器在之间意外弹出render()useEffect()它更新计时器回调函数。

显然,这是一个人为的示例,在现实生活中,您的组件可能要简单得多,并且实际上无法点击此窗口,但至少了解这一点是有好处的!

import { useEffect, useRef, useState } from 'react';

function useInterval(callback, delay, maxOccurrences) {
  const occurrencesRef = useRef(0);
  const savedCallback = useRef();

  // update ref before 2nd effect
  useEffect(() => {
    savedCallback.current = callback; // save the callback in a mutable ref
  });

  useEffect(() => {
    function tick() {
      // can always access the most recent callback value without callback dep
      savedCallback.current();
      occurrencesRef.current += 1;
      if (occurrencesRef.current >= maxOccurrences) {
        console.log(`max occurrences (delay ${delay})`);
        clearInterval(id);
      }
    }

    let id = setInterval(tick, delay);
    return () => clearInterval(id);
  }, [delay]);
}

function App() {
  const [num, setNum] = useState(0);
  const refNum = useRef(num);

  useInterval(() => console.log(`interval pop ${num} ${refNum.current}`), 0, 60);
  useInterval(() => setNum((n) => {
    refNum.current = n + 1;
    return refNum.current;
  }), 10, 20);

  return (
    <div className="App">
      <header className="App-header">
        <h1>Num: </h1>
      </header>
    </div>
  );
}

export default App;

选择useInterval()那没有同样的问题。

React 的关键是始终知道处理程序/闭包何时被调用。如果你使用setInterval()天真地使用任意函数那么你可能会遇到麻烦。但是,如果您确保仅在以下情况下调用处理程序useEffect()处理程序被调用,您将知道它们是在所有状态更新完成并且您处于一致状态后被调用的。因此,此实现不会像上面的实现那样受到影响,因为它确保在中调用不安全处理程序useEffect(),并且只调用安全处理程序setInterval():

import { useEffect, useRef, useState } from 'react';

function useTicker(delay, maxOccurrences) {
  const [ticker, setTicker] = useState(0);
  useEffect(() => {
    const timer = setInterval(() => setTicker((t) => {
      if (t + 1 >= maxOccurrences) {
        clearInterval(timer);
      }
      return t + 1;
    }), delay);
    return () => clearInterval(timer);
  }, [delay]);

  return ticker;
}

function useInterval(cbk, delay, maxOccurrences) {
  const ticker = useTicker(delay, maxOccurrences);
  const cbkRef = useRef();
  // always want the up to date callback from the caller
  useEffect(() => {
    cbkRef.current = cbk;
  }, [cbk]);

  // call the callback whenever the timer pops / the ticker increases.
  // This deliberately does not pass `cbk` in the dependencies as 
  // otherwise the handler would be called on each render as well as 
  // on the timer pop
  useEffect(() => cbkRef.current(), [ticker]);
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

在 useRef() 中存储回调 的相关文章

随机推荐