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

感谢阅读!

相关推荐
一个处女座的程序猿O(∩_∩)O2 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink5 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者6 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-7 小时前
验证码机制
前端·后端
燃先生._.8 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖9 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235249 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_7482402510 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar10 小时前
纯前端实现更新检测
开发语言·前端·javascript