次要免责声明:我不是核心react开发人员,我没有看过react代码,所以这个答案是基于阅读文档(字里行间)、经验和实验
也有人问过这个问题,因为它明确指出了useInterval()
实现的意外行为
从根本上反对在可变引用中存储回调吗?
我对 react 文档的阅读是不推荐这样做,但在某些情况下可能仍然是有用的甚至是必要的解决方案,因此“逃生舱口”参考,所以我认为答案是否定的。我认为不推荐,因为:
您明确拥有管理您正在保存的闭包的生命周期的所有权。当它过时时,你需要自己修复它。
这很容易以微妙的方式出错,见下文。
文档中给出了此模式作为如何解决在处理程序更改时重复呈现子组件的示例,正如文档所说:
最好避免在深处传递回调
通过例如使用上下文。这样,您的孩子每次重新渲染时都不太可能需要重新渲染。所以在这个用例中有一个更好的方法来做到这一点,但这将依赖于能够改变子组件。
但是,我确实认为这样做可以解决某些其他方式难以解决的问题,并且useInterval()
在您的代码库中测试和现场强化了这样的库函数的好处,其他开发人员可以使用,而不是尝试自己使用setInterval
直接(可能使用全局变量......这会更糟)将超过曾经useRef()
实现它的负面影响。如果有一个错误,或者一个由更新引入的react,只有一个地方可以修复它。
也可能是您的回调在过时时可以安全调用,因为它可能只是捕获了不变的变量。例如,由setState
返回的函数useState()
保证不会改变,请参阅this 中的最后一个注释,因此只要您的回调仅使用这样的变量,您就很漂亮。
话虽如此,setInterval()
您提供的实施确实存在缺陷,请参见下文,以及我建议的替代方案。
像上面的代码一样在并发模式下安全吗(如果不是,为什么)?
现在我不完全知道并发模式是如何工作的(而且它还没有最终确定 AFAIK),但我的猜测是并发模式很可能会加剧下面的窗口条件,因为据我所知它可能会将状态更新与渲染分开,增加窗口条件,即仅在useEffect()
触发时(即渲染时)更新的回调将在过期时调用。
显示您useInterval
可能会在过时时弹出的示例。
在下面的示例中,我演示了setInterval()
计时器可能会在 设置更新回调的setState()
和 的调用之间弹出useEffect()
,这意味着回调在过期时被调用,如上所述,这可能没问题,但可能导致到错误。
在示例中,我修改了您的内容setInterval()
,使其在出现某些情况后终止,并且我使用了另一个 ref 来保存num
. 我使用两个setInterval()
:
- 一个简单地记录
num
存储在 ref 和渲染函数局部变量中的值。
- 另一个定期更新
num
,同时更新中的值numRef
并调用setNum()
以导致重新渲染并更新局部变量。
现在,如果保证在调用下一次渲染setNum()
的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()
发生
- “间隔弹出”日志
useEffect()
更新 ref 被调用。
即定时器在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]);
}