你需要知道的 setTimeout 在浏览器下的限制

背景

在我们实现页面交互时,时常需要通过 「延迟执行」 来实现一些特定的功能。

比如使用 setTimeout 延迟执行渲染,来实现 「逐字输出」 效果。

如下代码示例:

html 复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title></title>
  </head>
  <body>
    <div id="text"></div>

    <script>
      const textElement = document.getElementById("text");
      const text =
        "春天,我从来没有强烈的感觉到春天在去年。有人说春天应该是一个快乐的季节。但我从来没有感觉到。我一直喜欢秋天,因为我认为秋天是一个浪漫的季节。我很喜欢夏天,因为我很年轻,我喜欢我的裙子和花边,现在,我还喜欢秋天和夏天,而我喜欢春天和冬天。以前我不喜欢各种颜色的花朵,我认为他们是不负责任的和肤浅的。我认为只有蓝色的海洋是深的,金色的秋天是优雅的。然而,现在我有一个不同的想法,我觉得春天美好。我喜欢在现场和在山的花。从他们身上我有生命的精神。";
      let index = 0;

      function typeWriter() {
        if (index < text.length) {
          textElement.textContent += text.charAt(index);
          index++;
          setTimeout(typeWriter, 100);
        }
      }
      typeWriter();
    </script>
  </body>
</html>

这段 setTimeout 计时器代码,在你正常浏览「当前页面」时,运行上不会有问题。

一旦你切换到其他浏览器 Tab 页,或是其他类似操作致使「当前页面」不在电脑屏幕的可见范围时,

你会发现:刚才页面进行中的 setTimeout 停止了工作(休眠),只有在你重新切换回来「当前页面」后,setTimeout 将从上次停止的位置继续工作

这其实与浏览器的优化机制(省电策略)有关:setTimeout()setInterval() 以及 requestAnimationFrame() 在浏览器窗口 「非激活」 的状态下会停止工作或者以极慢的速度工作

有一个 requestIdleCallback(),在浏览器窗口 「非激活」 的状态下不会停止工作,但是它没办法像 setTimeout 那样能够手动控制延迟时间。

但有时候这个优化并不是我们想要的,它会限制我们程序代码的正常执行。

方案

为了让浏览器窗口在非激活状态(或者最小化)下 计时器 有效不休眠,可以用 HTML5 的新特性:Web Workers 来解决。

Web Workers 是 HTML5 提供的一个 JavaScript 多线程解决方案,可以将一些大计算量的代码交由 Web Workers 运行而不冻结、阻塞用户界面。

基于 Web Workers 的特性,我们将 计时器 的执行放入 worker 子线程中,主线程只用不断接收,子线程在延迟时间过期后推送的通知就好了。

下面我们一起来看看具体的实现。

首先,我们定义一个 delayWorker.js 文件,作为开启子线程要执行的文件。计时器在这里注册,当延迟时间过期后,会向外推送消息,来执行对应的 callback 延迟函数。

js 复制代码
// delayWorker.js

onmessage = function (evt) {
  console.log("delayWorker.js 接收到用户传递的 data: ", evt);
  const { workId, time } = evt.data;
  // 在 worker 中开启计时器代码
  setTimeout(() => {
    postMessage(workId);
  }, time);
};

接着,我们封装一个 setTimeoutWork 方法来代替 setTimeout,它的用法与 setTimeout 一致。

js 复制代码
// setTimeoutWork.js

// 引入 work.js
const worker = new Worker("delayWorker.js");
// 收集计时器回调
const worksMap = new Map();

// 生成一个 32 位随机 id
const generateRandomId = (len = 32) => {
  let $chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
  let maxPos = $chars.length;
  let str = "";
  for (let i = 0; i < len; i++) {
    str += $chars.charAt(Math.floor(Math.random() * maxPos));
  }
  return str;
};

worker.onmessage = function (evt) {
  console.log("delayWorker.js 推送过来的 data: ", evt);
  const workId = evt.data;
  if (worksMap.has(workId)) {
    const { callback } = worksMap.get(workId);
    callback();
    worksMap.delete(workId);
  }
};

function setTimeoutWork(callback, time = 0) {
  // 为 work 创建唯一 id
  const workId = generateRandomId();
  worksMap.set(workId, { callback, time });
  worker.postMessage({ workId, time }); // 向 worker 发送数据
}

setTimeoutWork 的执行流程分析:

  1. 在使用 setTimeoutWork 时传入 callback 延迟回调函数 和 time 延迟时间;
  2. setTimeoutWork 为延迟任务生成一个唯一 id,并将 callback 存放到 Map 集合中;
  3. 接着向 Web Worker 子线程推送消息,在子线程中开启计时器代码的执行;
  4. 子线程会在计时器延迟时间达到以后,推送通知,work.onmessage 会接收到消息;
  5. 最后,通过唯一 idMap 集合中找到 callback 执行回调函数。

最后,上面的 「逐字输出」 示例可以改成 setTimeoutWork 去执行:

html 复制代码
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title></title>
  </head>
  <body>
    <div id="text"></div>

    <script src="./setTimeoutWork.js"></script>
    <script>
      const textElement = document.getElementById("text");
      const text =
        "春天,我从来没有强烈的感觉到春天在去年。有人说春天应该是一个快乐的季节。但我从来没有感觉到。我一直喜欢秋天,因为我认为秋天是一个浪漫的季节。我很喜欢夏天,因为我很年轻,我喜欢我的裙子和花边,现在,我还喜欢秋天和夏天,而我喜欢春天和冬天。以前我不喜欢各种颜色的花朵,我认为他们是不负责任的和肤浅的。我认为只有蓝色的海洋是深的,金色的秋天是优雅的。然而,现在我有一个不同的想法,我觉得春天美好。我喜欢在现场和在山的花。从他们身上我有生命的精神。";
      let index = 0;
      
      function typeWriter() {
        if (index < text.length) {
          textElement.textContent += text.charAt(index);
          index++;
          setTimeoutWork(typeWriter, 100);
        }
      }
      typeWriter();
    </script>
  </body>
</html>

感谢阅读!

相关推荐
sunly_30 分钟前
Flutter:自定义Tab切换,订单列表页tab,tab吸顶
开发语言·javascript·flutter
咔咔库奇1 小时前
【TypeScript】命名空间、模块、声明文件
前端·javascript·typescript
NoneCoder1 小时前
JavaScript系列(42)--路由系统实现详解
开发语言·javascript·网络
兩尛1 小时前
订单状态定时处理、来单提醒和客户催单(day10)
java·前端·数据库
又迷茫了1 小时前
vue + element-ui 组件样式缺失导致没有效果
前端·javascript·vue.js
哇哦Q2 小时前
原生HTML集合
前端·javascript·html
SoWhat~2 小时前
随遇随记篇
前端·javascript
孟健2 小时前
重磅首发:国产AI编程助手Trae实测!免费用上Claude是什么体验?
前端·aigc·visual studio code
爱上大树的小猪2 小时前
【前端SEO】使用Vue.js + Nuxt 框架构建服务端渲染 (SSR) 应用满足SEO需求
前端·javascript·vue.js
Java陈序员2 小时前
TypeScript 快速上⼿
前端·typescript