安全的setTimeout和setInterval

16 阅读3分钟

代码

这段代码每秒检测condition是否为真

function safeClearInterval(id) {
  if (!id) {
      return;
  }
  
  clearInterval(id);
}

function monitor(condition, interval=1000) {
  let timerId = null;
  timerId = setInterval(() => {
    if (!condition()) {
      safeClearInterval(timerId);
    }
  }, interval);
}

clearInterval是完全安全的

这段代码什么都不会做

clearInterval(null);

所以上面的safeClearInterval(id)函数可以移除

function monitor(condition, interval=1000) {
  let timerId = null;
  
  timerId = setInterval(() => {
    if (!condition()) {
      clearInterval(timerId);
    }
  }, interval);
}

整理

由于setInterval中的函数是异步执行,可以整理为

function monitor(condition, interval=1000) {
  const timerId = setInterval(() => {
    if (!condition()) {
      clearInterval(timerId);
    }
  }, interval);
}

向外暴露timerId

function monitor(condition, interval=1000) {
  const timerId = setInterval(() => {
    if (!condition()) {
      clearInterval(timerId);
    }
  }, interval);
  
  return timerId;
}

支持condition是异步函数

如果要支持condition是异步,那最好使用setTimeout代替interval,防止前一个condition还没结束,下一个condition就开始了

setTimeout 同步版本

每次调用setTimeout需要更新timerId,而timerId在浏览器环境是number类型(NodeJS环境是Timeout对象),直接返回外部无法监听更新,下面是错误的写法:

function monitor(condition, interval=1000) {
  let timerId = null;
  timerId = setTimeout(() => {
    if (condition()) {
      timerId = monitor(condition, interval);
    }
  }, interval);
  
  return timerId;
}

正确应该用对象包围,或返回id的getter

function monitor(condition, interval=1000, timer={ id: null }) {
  timer.id = setTimeout(() => {
    if (condition()) {
      monitor(condition, interval, timer);
    }
  }, interval);

  return timer;
}

function monitor(condition, interval=1000) {
  let timerId = null;
  timerId = setTimeout(() => {
    if (condition()) {
      monitor(condition, interval);
    }
  }, interval);

  const getTimerId = () => timerId;

  return getTimerId;
}

实践中不会对外暴露timerId,而是暴露stop函数,这样让模块内外都有更好的扩展性。

function monitor(condition, interval=1000, timer={ id: null }) {
  timer.id = setTimeout(() => {
    if (condition()) {
      monitor(condition, interval);
    }
  }, interval);

  function stop() {
    clearTimeout(timer.id);
  }
  
  return stop;
}

注意到,可以更规范地对外返回start函数,让外部控制何时开始

function monitor(condition, interval=1000) {
  let timerId = null;
  
  function start() {
    timerId = setTimeout(() => {
      if (condition()) {
        start();
      }
    }, interval);
  }

  function stop() {
    clearTimeout(timerId);
  }
  
  return {start, stop};
}

更多地,start可以接受一个immediate参数

function start(immediate=false) {
  if (immediate && !condition()) {
    return;
  }

  timerId = setTimeout(() => {
    if (condition()) {
      start();
    }
  }, interval);
}

每次循环都判断immediate && !condition()的返回值是低效的,应该把循环的代码提取出来:

function monitor(condition, interval=1000) {
  let timerId = null;
  
  function _loop() {
      timerId = setTimeout(() => {
      if (condition()) {
        _loop();
      }
    }, interval);
  }
  
  function start(immediate=false) {
    if (immediate && !condition()){
      return;
    }
    
    _loop();
  }

  function stop() {
    clearTimeout(timerId);
  }
  
  return {start, stop};
}

setTimeout 异步版本

即使condition为同步函数,await condition()也会将其包装为一个Promise,不需要开发者处理边缘情况。

function monitor(condition, interval=1000) {
  let timerId = null;
  
  async function _loop() {
      timerId = setTimeout(async () => {
      if (await condition()) {
        _loop();
      }
    }, interval);
  }
  
  async function start(immediate=false) {
    if (immediate && !(await condition())){
      return;
    }
    
    _loop();
  }

  function stop() {
    clearTimeout(timerId);
  }
  
  return {start, stop};
}

竞态条件处理

在执行setTimout()的回调函数执行时,在if (await condition())等待condition返回时,如果外部调用stop()condition函数返回后,依旧会调用_loop,这意味着,该情况下stop调用是无效的。可以这样修复:

function monitor(condition, interval=1000) {
  let timerId = null;
  let isStopped = true;
  
  async function _loop() {
    if (isStopped) {
      return;
    }
  
    timerId = setTimeout(async () => {
      if (await condition()) {
        _loop();
      }
    }, interval);
  }
  
  async function start(immediate=false) {
    if (!isStopped) return; // 正在运行则直接返回
    isStopped = false;
  
    if (immediate && !(await condition())){
      isStopped = true;
      return;
    }
    
    _loop();
  }

  function stop() {
    isStopped = true;
    clearTimeout(timerId);
  }
  
  return {start, stop};
}

补充

虽然前面的代码具有一定的稳定性,但是在等待if (immediate && !(await condition())返回结果时调用stop,依旧需要condition执行完毕,如果condition需要10秒,那就会多进行10秒无意义的运行。这个问题可以借助AbortController解决。