使用递归 setTimeout 启动和停止计时器

Start and Stop a Timer with Recursive setTimeout

我正在尝试使用递归 setTimeout 启动和停止计时器,因为 setInterval 不符合我的需要。当用户按下开始时,计时器开始计时。

1...2...3...4..5...1...2...3..4...5...1..2... 3..4...5...

并且当用户按下停止键时,它应该停止这个定时器循环。如果我再次按下开始,改变按钮的状态(流变量),计时器应该再次做同样的事情(1..2..3..4..5..1..2...3。 .4...5..)

我想应该是这样的。


const ref = useRef(null);
useEffect(() => {
  if (!stream) {
    ref.current=setTimeout(repeatingFunc, 1000);

  } else {
    clearTimeout(ref.current);
  }
}, [stream]);

function repeatingFunc() {
  console.log("It's been 5 seconds. Execute the function again.");
  setTimeout(repeatingFunc, 5000);
}

我做错了什么?

计时器不会停止。即使我按下停止,它也会继续运行! (更改流状态值!)

您可以像这样定义自定义计时器:

function CustomTimer(func) {
    this.id = -1;
    this.func = func;

    this.start = function (interval) {
        this.stop();

        if (this.func) {
            var t = this;
            this.id = setTimeout(function () { t.func.call(t) }, interval || 1000);
        }
    }

    this.stop =  function () {
        if (this.id >= 0) {
            clearTimeout(this.id);
            this.id = -1;
        }
    }
}

并使用它:

var myTimer = new CustomTimer(function () {
    console.log("It's been 5 seconds. Execute the function again.");
    this.start(5000);
});

myTimer.start(5000);

您可以更改在任何时刻执行的函数以及间隔。例如:运行 1 秒内开始事件,然后每 5 秒 5 次做其他事情:

var secondFunc = function () {
    console.log("It's been 5 seconds. Execute the function again.");

    if (++this.count < 5)
        this.start(5000);
}

var myTimer = new CustomTimer(function () {
    console.log("First event at 1 second");

    this.count = 0;
    this.func = secondFunc;
    this.start(5000);
});

myTimer.start(1000);

计时器不停止的原因是因为您只存储了初始 setTimeout 调用的超时 ID。当 repeatingFunc 被调用时,另一个 setTimeout 回调被注册,它有一个新的超时 ID。

因此,当您尝试使用 clearTimeout(ref.current) 清除超时时,您传递的是过时的超时 ID。

由于之前的setTimeout注册已经调用,您不再需要存储之前的ID,只需将其替换为新的即可。

const ref = useRef(null);

useEffect(() => {
  if (!stream) {
    ref.current = setTimeout(repeatingFunc, 1000);
  } else {
    clearTimeout(ref.current);
  }
}, [stream]);

function repeatingFunc() {
  console.log("It's been 5 seconds. Execute the function again.");
  ref.current = setTimeout(repeatingFunc, 5000);
  // ^^^^^^^^^^^^ update timeout ID after registering a new timeout
}

请注意,repeatingFunc 的范围固定在您最初调用 setTimeout(repeatingFunc, 1000) 的时间点,这意味着状态和属性等内容可能包含过时的值。这可能会或可能不会成为问题,具体取决于您的上下文。


您可能还想添加一个 useEffect 清理函数,用于清除组件卸载时的超时。否则,即使组件不再安装,您的循环也会继续。

useEffect(() => {
  if (!stream) {
    ref.current = setTimeout(repeatingFunc, 1000);
  }

  return () => clearTimeout(ref.current);
}, [stream]);

假设 stream 是一个布尔值 (true/false) 以上将在 stream 的值更改或组件卸载时清除当前超时.

streamtrue变为false时,会调用清理函数,清除超时。

streamfalse 变为 true 时,清理函数也会被调用,但结果是 no-op 因为当前的 ref.current 不是注册时间更长。然后设置新的超时。

Passing an invalid ID to clearTimeout() silently does nothing; no exception is thrown.

参见:MDN - clearTimeout()