“官方”useInterval示例中的潜在错误

使用间隔

useInterval来自Dan Abramov 的这篇博文(2019 年):

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

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

一个潜在的错误

可以在提交阶段和调用之间调用间隔回调useEffect,从而导致调用旧的(因此不是最新的)回调。换句话说,这可能是执行顺序:

  1. 渲染阶段- 的新值callback
  2. 提交阶段- 提交给 DOM 的状态。
  3. 使用布局效果
  4. 间隔回调- using savedCallback.current(),与callback.
  5. 使用效果-savedCallback.current = callback;

React 的生命周期

为了进一步说明这一点,这里有一张图表,显示了带有钩子的 React 生命周期:

虚线表示异步流(事件循环已释放),您可以在这些点进行间隔回调调用。

该图显示了并发模式RenderReact updates DOM(提交阶段)之间的虚线是异步的。正如此代码和框所示,您只能在useLayoutEffector之后useEffect(但不能在渲染阶段之后)调用间隔回调。

所以你可以在 3 个地方设置回调:

  • 渲染 - 不正确,因为状态更改尚未提交到 DOM。
  • useLayoutEffect - 正确,因为状态更改已提交到 DOM。
  • useEffect - 不正确,因为旧的间隔回调可能会在此之前触发(在布局效果之后)。

演示

这个错误在这个codeandebox 中得到了证明。重现:

  • 将鼠标移到灰色 div 上 - 这将导致带有新callback参考的新渲染。
  • 通常,您会看到在少于 2000 次鼠标移动时抛出的错误。
  • 间隔设置为 50 毫秒,因此您需要一点运气才能在渲染和效果阶段之间触发。

用例

演示显示当前的回调值可能与useEffect正常的不同,但真正的问题是其中哪一个是“正确的”

考虑这个代码:

const [size, setSize] = React.useState();

const onInterval = () => {
  console.log(size)
}

useInterval(onInterval, 100);

如果onInterval在提交阶段之后但之前调用useEffect,它将打印错误的值。

回答

尽管我理解讨论,但这对我来说看起来不像是一个错误。

上面建议在渲染期间更新 ref 的答案是一个副作用,应该避免,因为它会导致问题。

演示显示当前回调值可能与 useEffect 中的不同,但真正的问题是其中哪一个是“正确”的?

我相信“正确”的就是已经承诺的那个。出于一个原因,提交的效果是唯一可以保证稍后有清理阶段的效果。(这个问题中的间隔不需要清理效果,但其他事情可能会。)

在这种情况下,另一个更令人信服的原因可能是 React 可能会预渲染事物(或者以较低的优先级,或者因为它们“离屏”并且尚不可见,或者在未来的动画 API 中)。像这样的预渲染工作永远不应该修改 ref,因为修改是任意的。(考虑一个未来的动画 API,它预渲染多个可能的未来视觉状态,以响应用户交互更快地进行转换。您不希望最后渲染的那个只是改变当前可见/提交使用的 ref看法。)


编辑 1这个讨论似乎主要是指出,当 JavaScript 不同步(阻塞)时,当它在渲染之间产生时,有可能在两者之间发生其他事情(例如之前安排的计时器/间隔)。这是真的,但我认为如果在渲染期间(在“提交”更新之前)发生这种情况这不是错误。

如果主要担心回调可能会在 UI 提交后执行并且与屏幕上的内容不匹配,那么您可能需要考虑useLayoutEffect。这种效果类型在提交阶段被调用,在 React 修改了 DOM 之后但在 React 返回给浏览器之前(也就是没有间隔或计时器可以在两者之间运行)。


编辑 2我相信 Dan 最初建议为此使用 ref 和效果(而不仅仅是效果)的原因是因为对回调的更新不会重置间隔。(如果您调用clearInterval并且setInterval每次回调更改,则整体计时将被中断。)


以上是“官方”useInterval示例中的潜在错误的全部内容。
THE END
分享
二维码
< <上一篇
下一篇>>