你需要知道的 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>

感谢阅读!

相关推荐
excel2 分钟前
webpack 核心编译器 十四 节
前端
excel8 分钟前
webpack 核心编译器 十三 节
前端
腾讯TNTWeb前端团队7 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰10 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪11 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪11 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy11 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom12 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom12 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom12 小时前
React与Next.js:基础知识及应用场景
前端·面试·github