bel*_*a53 14 javascript reactjs react-ref react-hooks react-concurrent
下面是一个可变 ref 的示例,该示例存储来自Overreacted 博客的当前回调:
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]);
}
Run Code Online (Sandbox Code Playgroud)
然而,React Hook FAQ 指出不推荐使用该模式:
另请注意,此模式可能会导致并发模式出现问题。[...]
在任何一种情况下,我们都不推荐这种模式,只是为了完整起见在这里展示它。
我发现这种模式对于回调非常有用,但我不明白为什么它会在 FAQ 中出现危险信号。例如,客户端组件可以使用useInterval而无需useCallback环绕回调(更简单的 API)。
在并发模式下也不应该有问题,因为我们更新了useEffect. 在我看来,FAQ 条目在这里可能有一个错误的观点(或者我误解了它)。
所以,总结一下:
次要免责声明:我不是核心反应开发人员,我没有看过反应代码,所以这个答案是基于阅读文档(字里行间)、经验和实验
也有人问过这个问题,因为它明确指出了useInterval()实现的意外行为
我对 react 文档的阅读是不推荐这样做,但在某些情况下可能仍然是有用的甚至是必要的解决方案,因此“逃生舱口”参考,所以我认为答案是否定的。我认为不推荐,因为:
您明确拥有管理您正在保存的闭包的生命周期的所有权。当它过时时,你需要自己修复它。
这很容易以微妙的方式出错,见下文。
文档中给出了此模式作为如何解决在处理程序更改时重复呈现子组件的示例,正如文档所说:
最好避免在深处传递回调
通过例如使用上下文。这样,您的孩子每次重新渲染时都不太可能需要重新渲染。所以在这个用例中有一个更好的方法来做到这一点,但这将依赖于能够改变子组件。
但是,我确实认为这样做可以解决某些其他方法难以解决的问题,并且useInterval()在您的代码库中测试和现场强化了这样的库函数的好处,其他开发人员可以使用它,而不是尝试自己使用setInterval直接(可能使用全局变量......这会更糟)将超过曾经useRef()实现它的负面影响。如果有一个错误,或者一个由更新引入的反应,只有一个地方可以修复它。
也可能是您的回调在过时时可以安全调用,因为它可能只是捕获了不变的变量。例如,由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)
Run Code Online (Sandbox Code Playgroud)
并且每次数字不同时说明回调已在调用之后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;
Run Code Online (Sandbox Code Playgroud)
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]);
}
Run Code Online (Sandbox Code Playgroud)