小免责声明:我不是核心 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()
.
添加更多跟踪后,差异日志的顺序显示为:
-
setNum()
叫做,
-
render()
occurs
- “间隔弹出”日志
-
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]);
}