react-hooks和 setInterval

IT技术 reactjs carousel setinterval react-hooks
2021-04-01 13:40:26

除了在后台保留一个“时钟”以使用react-hooks在轮播中实现自动下一步(几秒钟后)之外,还有其他选择吗?

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

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
  };
};
4个回答

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

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

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

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 为我们提供了这个useRefuseRef函数将返回一个具有current属性的对象然后我们可以将该属性(每次组件重新渲染时都会发生)设置为counter的当前版本setCounter

(第二个示例,使用功能性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]
);

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

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

您可以更改setIntervalsetTimeout,您将获得完全相同的行为。setTimeout不是一个持久的时钟,但这并不重要,因为无论如何它们都会被清理干净。

如果您根本不想启动任何计时器,请将条件放在setIntervalnot 里面。

  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);
        }
      };
    }
  );
嗯,沙箱对我来说工作得很好,这很奇怪。在理论上不需要传递任何第二个参数来使这个工作,而且它在实践中似乎也有效。你能复制代码并粘贴到某个地方吗?想知道是否存在缓存差异或其他内容
2021-05-29 13:40:26
codesanbox 似乎不起作用——只重新渲染一次。另外,在我的示例中,我没有有条件地设置 setInterval有条件地增加处理程序函数内的看看这里:codesandbox.io/s/5wxpnqn9kx
2021-06-01 13:40:26
我理解你关于 setInterval 和 setTimeout 的观点——如果 useEffect 中没有依赖关系,那么它会在每次运行时为下一次渲染设置一个计时器。
2021-06-05 13:40:26

到目前为止,似乎以下两种解决方案都按预期工作:

有条件创建计时器-它需要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

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