【问题标题】:react hooks and setInterval反应钩子和 setInterval
【发布时间】:2019-05-27 15:22:12
【问题描述】:

除了在后台保持“时钟”以使用反应钩子在轮播中实现自动下一步(几秒钟后)之外,还有其他选择吗?

下面的自定义反应钩子实现了轮播的状态,支持手动(下一个、上一个、重置)和自动(开始、停止)方法来更改轮播的当前(活动)索引。

const useCarousel = (items = []) => {
  const [current, setCurrent] = useState(
    items && items.length > 0 ? 0 : undefined
  );

  const [auto, setAuto] = useState(false);

  const next = () => setCurrent((current + 1) % items.length);
  const prev = () => setCurrent(current ? current - 1 : items.length - 1);
  const reset = () => setCurrent(0);
  const start = _ => setAuto(true);
  const stop = _ => setAuto(false);


useEffect(() => {
    const interval = setInterval(_ => {
      if (auto) {
        next();
      } else {
        // do nothing
      }
    }, 3000);
    return _ => clearInterval(interval);
  });

  return {
    current,
    next,
    prev,
    reset,
    start,
    stop
  };
};

【问题讨论】:

  • 你的问题到底是什么?如何在没有“时钟”的情况下按时间间隔做某事?
  • 另外,因为状态在每个时间间隔都会更新,并且useEffect 有一个清除它的返回函数,它会在每次渲染时启动和停止一个新的时钟。 setTimeout 在这种情况下应该完全一样
  • 有没有其他方法可以让时钟在不被使用的情况下始终在后台运行? — 我尝试在 start 方法中使用 useEffect 但没有结果
  • 你不能调用useEffect 或在另一个函数体内或在条件后面的任何钩子。它们需要在主渲染体中无条件运行
  • 我有..我已经阅读了很多次文档并且还大量使用了钩子。您可以通过将控制台日志放在清理函数的主体中来轻松地自行测试

标签: reactjs carousel setinterval react-hooks


【解决方案1】:

setIntervalsetTimeout 之间存在一些差异,您可能不希望在组件重新渲染时始终重新启动计时器来丢失这些差异。 This fiddle 显示了当其他代码也在运行时两者之间的漂移差异。 (在较旧的浏览器/机器上——就像我最初回答这个问题时一样——你甚至不需要模拟一个大型计算就可以在几秒钟后看到明显的漂移。)

现在参考your answer,Marco,setInterval 的使用完全丢失了,因为每次组件重新渲染时无条件处理和重新运行的效果。因此,在您的第一个示例中,current 依赖项的使用会导致该效果在每次 current 更改(每次间隔运行时)时释放并重新运行。第二个做同样的事情,但实际上每次任何状态发生变化(导致重新渲染),这都可能导致一些意想不到的行为。一个工作的唯一原因是因为next() 导致状态更改。

考虑到您可能不关心确切时间的事实,以简单的方式使用setTimeout 是最干净的,使用currentauto 变量作为依赖项。因此,要重新陈述您的部分答案,请执行以下操作:

useEffect(
  () => {
    if (!auto) return;
    const interval = setTimeout(_ => {
      next();
    }, autoInterval);
    return _ => clearTimeout(interval);
  },
  [auto, current]
);

一般来说,对于那些刚刚阅读此答案并想要一种方法来做一个简单计时器的人,这里的版本没有考虑到 OP 的原始代码,也没有他们需要一种独立启动和停止计时器的方法:

const [counter, setCounter] = useState(0);
useEffect(
  () => {
    const id= setTimeout(() => {
      setCounter(counter + 1); 
      // You could also do `setCounter((count) => count + 1)` instead.
      // If you did that, then you wouldn't need the dependency
      // array argument to this `useEffect` call.
    }, 1000);
    return () => {
      clearTimeout(id);
    };
  },
  [counter],
);

但是,您可能想知道如何使用更精确的间隔,因为setTimeout 可以比setInterval 漂移更多。这是一种不使用 OP 代码的通用方法:

// Using refs:

const [counter, setCounter] = useState(30);
const r = useRef(null);
r.current = { counter, setCounter };
useEffect(
  () => {
    const id = setInterval(() => {
      r.current.setCounter(r.current.counter + 1);
    }, 1000);
    return () => {
      clearInterval(id);
    };
  },
  [] // empty dependency array
);

// Using the function version of `setCounter` is cleaner:

const [counter, setCounter] = useState(30);
useEffect(
  () => {
    const id = setInterval(() => {
      setCounter((count) => count + 1);
    }, 1000);
    return () => {
      clearInterval(id);
    };
  },
  [] // empty dependency array
);

这是上面发生的事情:

(第一个示例,使用 refs):要让 setInterval 的回调始终引用当前可接受的 setCounter 版本,我们需要一些可变状态。 React 用useRef 给了我们这个。 useRef 函数将返回一个具有current 属性的对象。然后我们可以将该属性(每次组件重新渲染时都会发生)设置为countersetCounter 的当前版本。

(第二个例子,使用函数式setCounter:和第一个思路一样,只是当我们使用setCounter的函数版本时,我们将可以访问当前版本的算作函数的第一个参数。无需使用 ref 来保持最新状态。

(两个例子,继续):然后,为了避免在每次渲染时释放间隔,我们添加一个空的依赖数组作为useEffect 的第二个参数。卸载组件时,间隔仍然会被清除。

注意:我曾经喜欢使用["once"] 作为我的依赖数组来表明我强制这个效果只设置一次。当时它的可读性很好,但我不再使用它有两个原因。首先,这些天钩子被更广泛地理解,我们已经看到到处都是空数组。其次,它与非常流行的“钩子规则”linter 冲突,后者对依赖数组中的内容非常严格。

因此,将我们所知道的应用于 OP 的原始问题,您可以使用 setInterval 来制作这样一个不太可能漂移的幻灯片:

// ... OP's implementation code including `autoInterval`,
// `auto`, and `next` goes above here ...

const r = useRef(null);
r.current = { next };
useEffect(
  () => {
    if (!auto) return;
    const id = setInterval(() => {
      r.current.next();
    }, autoInterval);
    return () => {
      clearInterval(id);
    };
  },
  [auto]
);

【讨论】:

    【解决方案2】:

    因为current 值会在每个“间隔”上发生变化,只要它应该运行,那么您的代码将在每次渲染时启动和停止一个新计时器。你可以在这里看到它的实际效果:

    https://codesandbox.io/s/03xkkyj19w

    您可以将setInterval 更改为setTimeout,您将获得完全相同的行为。 setTimeout 不是持久时钟,但没关系,因为它们都被清理了。

    如果您根本不想启动任何计时器,则将条件放在setInterval 之前,不要放在其中。

      useEffect(
        () => {
          let id;
    
          if (run) {
            id = setInterval(() => {
              setValue(value + 1)
            }, 1000);
          }
    
          return () => {
            if (id) {
              alert(id) // notice this runs on every render and is different every time
              clearInterval(id);
            }
          };
        }
      );
    

    【讨论】:

    • codesanbox 似乎无法正常工作 - 只会重新渲染一次。此外,在我的示例中,我没有有条件地设置 setInterval。我在处理函数中有条件地递增值。看这里:codesandbox.io/s/5wxpnqn9kx
    • 我理解你关于 setInterval 和 setTimeout 的观点——如果 useEffect 中没有依赖项,那么它将在每次运行时为下一次渲染设置一个计时器。
    • 嗯,沙盒对我来说工作得很好,这很奇怪。理论上不需要传递任何第二个参数来使这项工作有效,而且在实践中它似乎也有效。您可以复制代码并将其粘贴到某个地方吗?想知道是否有缓存的差异或什么
    【解决方案3】:

    到目前为止,似乎以下两种解决方案都可以正常工作:

    有条件地创建计时器 — 它要求 useEffect 依赖于 autocurrent 才能工作

    useEffect(
        () => {
          if (!auto) return;
          const interval = setInterval(_ => {
            next();
          }, autoInterval);
          return _ => clearInterval(interval);
        },
        [auto, current]
      );
    

    有条件地执行状态更新——它不需要 useEffect 依赖项

    useEffect(() => {
        const interval = setInterval(_ => {
          if (auto) {
            next();
          } else {
            // do nothing
          }
        }, autoInterval);
        return _ => clearInterval(interval);
      });
    

    如果我们将setInterval 替换为setTimeout,这两种解决方案都有效

    【讨论】:

      【解决方案4】:

      您可以使用useTimeout 钩子在指定的毫秒数后返回true

      【讨论】:

        猜你喜欢
        • 2021-01-16
        • 2019-11-29
        • 2019-08-29
        • 2019-09-09
        • 1970-01-01
        • 2021-07-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多