乐趣区

关于前端:大家都能看得懂的源码如何让定时器在页面最小化的时候不执行

本文是深入浅出 ahooks 源码系列文章的第七篇,该系列已整顿成文档 - 地址。感觉还不错,给个 star 反对一下哈,Thanks。

明天咱们来聊聊定时器。

useInterval 和 useTimeout

看名称,咱们就能大略晓得,它们的性能对应的是 setInterval 和 setTimeout,那比照后者有什么劣势?

先看 useInterval,代码简略,如下所示:

function useInterval(fn: () => void,
  delay: number | undefined,
  options?: {immediate?: boolean;},
) {
  const immediate = options?.immediate;
  const fnRef = useLatest(fn);

  useEffect(() => {
    // 疏忽局部代码...
    // 立刻执行
    if (immediate) {fnRef.current();
    }
    const timer = setInterval(() => {fnRef.current();
    }, delay);
    // 革除定时器
    return () => {clearInterval(timer);
    };
    // 动静批改 delay 以实现定时器距离变动与暂停。}, [delay]);
}

跟 setInterval 的区别如下:

  • 能够反对第三个参数,通过 immediate 可能立刻执行咱们的定时器。
  • 在变更 delay 的时候,会主动革除旧的定时器,并同时启动新的定时器。
  • 通过 useEffect 的返回革除机制, 开发者不须要关注革除定时器的逻辑,防止内存泄露问题 。这点是很多开发者会疏忽的点。

useTimeout 跟下面很相似,如下所示,不再做额定解释:

function useTimeout(fn: () => void, delay: number | undefined): void {const fnRef = useLatest(fn);

  useEffect(() => {
    // ... 疏忽局部代码
    const timer = setTimeout(() => {fnRef.current();
    }, delay);
    return () => {clearTimeout(timer);
    };
  // 动静批改 delay 以实现定时器距离变动与暂停。}, [delay]);
}

setTimeout 和 setInterval 的问题

首先,setTimeout 和 setInterval 作为事件循环中宏工作的“两大主力”,它的执行机会不能跟咱们预期一样精确的,它须要期待后面工作的执行。比方上面的 setTimeout 的第二个参数设置为 0,并不会立刻执行。

setTimeout(() => {console.log('test');
}, 0)

另外还有一种状况,setTimeout 和 setInterval 在浏览器不可见的时候(比方最小化的时候),不同的浏览器中设置不同的工夫距离的时候,其体现不一样。依据 当浏览器切换到其余标签页或者最小化时,你的 js 定时器还准时吗?这篇文章的实际论断如下:

谷歌浏览器中,当页面处于不可见状态时,setInterval 的最小间隔时间会被限度为 1s。火狐浏览器的 setInterval 和谷歌个性统一,然而 ie 浏览器没有对不可见状态时的 setInterval 进行性能优化,不可见前后间隔时间不变。

在谷歌浏览器中,setTimeout 在浏览器不可见状态下距离低于 1s 的会变为 1s,大于等于 1s 的会变成 N +1s 的距离值。火狐浏览器下 setTimeout 的最小间隔时间会变为 1s, 大于等于 1s 的距离不变。ie 浏览器在不可见状态前后的间隔时间不变。

这个论断,我没有验证过,但看起来差别挺大,其中还提到了另外一个抉择,就是 requestAnimationFrame。

window.requestAnimationFrame() 通知浏览器——你心愿执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该办法须要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

为了进步性能和电池寿命,因而在大多数浏览器里,当 requestAnimationFrame() 运行在后盾标签页或者暗藏的 <iframe> 里时,requestAnimationFrame() 会被暂停调用以晋升性能和电池寿命

所以,ahooks 也提供了应用 requestAnimationFrame 进行模仿定时器解决的 hook,咱们一起来看下。

useRafInterval 和 useRafTimeout

间接看 useRafInterval。(useRafTimeout 和 useRafInterval 相似,这里不开展细说)。

function useRafInterval(fn: () => void,
  delay: number | undefined,
  options?: {immediate?: boolean;},
) {
  const immediate = options?.immediate;
  const fnRef = useLatest(fn);

  useEffect(() => {
    // 省略局部代码...
    const timer = setRafInterval(() => {fnRef.current();
    }, delay);
    return () => {clearRafInterval(timer);
    };
  }, [delay]);
}

能够看到,跟后面的 useInterval 大部分代码逻辑都是一样的,只是定时应用了 setRafInterval 办法,革除定时器用了 clearRafInterval

setRafInterval

间接上代码:

const setRafInterval = function (callback: () => void, delay: number = 0): Handle {if (typeof requestAnimationFrame === typeof undefined) {
    // 如果不反对,还是应用 setInterval
    return {id: setInterval(callback, delay),
    };
  }
  // 开始工夫
  let start = new Date().getTime();
  const handle: Handle = {id: 0,};
  const loop = () => {const current = new Date().getTime();
    // 以后工夫 - 开始工夫,大于设置的距离,则执行,并重置开始工夫
    if (current - start >= delay) {callback();
      start = new Date().getTime();
    }
    handle.id = requestAnimationFrame(loop);
  };
  handle.id = requestAnimationFrame(loop);
  return handle;
};

首先是用 typeof 判断进行兼容逻辑解决,如果不兼容,则兜底应用 setInterval。

初始记录一个 start 的工夫。

在 requestAnimationFrame 回调中,判断当初的工夫减去开始工夫有没有达到距离,如果达到则执行咱们的 callback 函数。更新开始工夫。

clearRafInterval

革除定时器。

function cancelAnimationFrameIsNotDefined(t: any): t is NodeJS.Timer {return typeof cancelAnimationFrame === typeof undefined;}

// 革除定时器
const clearRafInterval = function (handle: Handle) {if (cancelAnimationFrameIsNotDefined(handle.id)) {return clearInterval(handle.id);
  }
  cancelAnimationFrame(handle.id);
};

如果不反对 cancelAnimationFrame API,则通过 clearInterval 革除,反对则间接应用 cancelAnimationFrame 革除。

思考与总结

对于定时器,咱们平时用得不少,但常常有同学容易遗记革除定时器,联合 useEffect 返回革除副作用函数这个个性,咱们能够将这类逻辑一起封装到 hook 中,让开发者应用更加不便。

另外,如果心愿在页面不可见的时候,不执行定时器,能够抉择 useRafInterval 和 useRafTimeout,其外部是应用 requestAnimationFrame 进行实现。

退出移动版