定时器在页面最小化时不执行实现示例

2022-07-13,,,,

引言

本文是深入浅出 ahooks 源码系列文章的第七篇,这个系列的目标主要有以下几点:

  • 加深对 react hooks 的理解。
  • 学习如何抽象自定义 hooks。构建属于自己的 react hooks 工具库。
  • 培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。

注:本系列对 ahooks 的源码解析是基于 v3.3.13。自己 folk 了一份源码,主要是对源码做了一些解读,可见 详情。

今天我们来聊聊定时器

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 在浏览器不可见的时候(比如最小化的时候),不同的浏览器中设置不同的时间间隔的时候,其表现不一样。根据 这篇文章的实践结论如下:

谷歌浏览器中,当页面处于不可见状态时,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 进行实现。

以上就是定时器在页面最小化时不执行实现示例的详细内容,更多关于定时器页面最小化不执行的资料请关注其它相关文章!

《定时器在页面最小化时不执行实现示例.doc》

下载本文的Word格式文档,以方便收藏与打印。