背景
在我们实现页面交互时,时常需要通过 「延迟执行」 来实现一些特定的功能。
比如使用 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
的执行流程分析:
- 在使用
setTimeoutWork
时传入callback
延迟回调函数 和time
延迟时间; setTimeoutWork
为延迟任务生成一个唯一id
,并将callback
存放到Map
集合中;- 接着向
Web Worker
子线程推送消息,在子线程中开启计时器代码的执行; - 子线程会在计时器延迟时间达到以后,推送通知,
work.onmessage
会接收到消息; - 最后,通过唯一
id
在Map
集合中找到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>
感谢阅读!