前言:当界面卡顿时,Web Worker 来救场
你有没有遇到过这样的场景——点击网页上的某个按钮后,整个页面突然 “冻结” 了,无法滚动、无法点击其他元素,甚至连动画都变得一卡一卡的?这正是JavaScript单线程特性带来的“副作用”:当浏览器忙于执行一段耗时操作(比如大量计算)时,它会阻塞主线程,导致页面失去响应。
想象这样一个页面:一个按钮控制着方块的变色,另一个按钮触发一段从1数到400000000的“疯狂计数”,并在完成后弹出提示。如果没有优化,点击计数按钮后,你会立刻发现——方块变色的动画卡住了,页面像被“冻住”一样,直到计数完成才能恢复。
这就是Web Worker的用武之地!通过将耗时任务交给后台线程处理,Web Worker能让主线程专注响应用户交互,保持页面流畅。本文将带你用Web Worker,解决“页面卡顿”的痛点。
什么是 Web Worker ?
Web Worker 是 HTML5 提供的浏览器多线程解决方案,允许在后台线程中运行 JavaScript 代码,避免阻塞主线程(UI 线程)。
它经常被用来执行耗时性的代码,避免主线程被阻塞。
拿上面这个例子来说吧:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="container">
<div class="box box1"></div>
<div class="box box2"></div>
<div class="box box3"></div>
</div>
<div>
<button id="btn">Change colors</button>
<button id="count">Alert</button>
</div>
<script>
let boxes = document.querySelectorAll('.box')
let btn = document.getElementById('btn')
let isOriginalColor = true // 获取元素
---------------------------------------------------------------
btn.addEventListener('click', changeColor) // 来回切换颜色
function changeColor() {
boxes.forEach(box => {
if (isOriginalColor) {
box.style.backgroundColor = 'rgb(22,87,172)'
} else {
box.style.backgroundColor = 'rgb(22,172,172)'
}
})
isOriginalColor = !isOriginalColor
}
----------------------------------------------------------------
let count = document.getElementById('count'); // 进行计数(耗时性操作)
count.addEventListener('click', alertCount);
function alertCount() {
for (let i = 0; i <= 4000000000; i++) {
if (i === 4000000000) {
alert(`Reached ${i}`)
}
}
}
-------------------------------------------------------
</script>
</body>
</html>
在这个页面中,切换颜色这一行为几乎可以立刻完成,因为它没有那么复杂,也不耗时,但是计数这个操作,已经达到了十亿级别,会特别耗时,比如上面就几乎用了10s......
那么既然知道了计数操作是个耗时操作,而且会阻塞页面的运行,那么能不能把它交给Web Worker?
包能的老弟,接下来看我给你讲!
Web Worker 怎么用 ?
首先我们要创建一个Web Worker(我把HTML中的JS,放入一个main.js文件了):
// main.js
const worker = new Worker('worker.js')
-----------------
// 原来的代码放这里
new Worker()中所填写的是Web Worker的路径,Web Worker本质上就是另一个JS脚本文件,而这个文件被浏览器的另一个线程所执行。
这个时候我们就可以新建一个worker.js文件了。
如何传递信息 ?
Worker的运行是这样的:
- 由主线程的JS给Worker.js发送信息
- worker.js 收到信息,开始执行一段逻辑
- woker.js 返回结果
- main.js 接收结果
主线程发送信息
main.js中需要这样发送信息:
// main.js
const worker = new Worker('worker.js')
worker.postMessage('HI')
worker.postMessage()
worker.postMessage() 方法的标准语法是:
worker.postMessage(message, [transferList]);
message(必填):要传递给 Worker 的数据(支持结构化克隆算法支持的类型)。
transferList(可选):用于转移 ArrayBuffer、MessagePort 等可转移对象的所有权。
当然如果你想传输多条数据,可以封装为一个对象传递过去:
{
data1:666
data2:'Good'
data3:{name:'Skye',age:'20'}
}
但是注意咯,message中不可以传递函数或者Symbol等值。
worker.js 接收信息(onmessage)
它需要接收信息,接收信息后会立即执行一个函数:
// worker.js
self.onmessage = function (e) {
// .....
}
self相当于worker.js的全局对象,像window一样。
onmessage像一个监听器,和addEventListener差不多,来监听收到的信息,一旦收到信息,就执行下面的函数。
而接收的形参e,就是一个MessageEvent,它长这样:
其中的data就是我们从main.js传过来的数据。
这样大家就知道该怎么做了,我们只需要把耗时操作复制过来,让这个JS执行就好了:
self.onmessage = function (e) {
for (let i = 0; i <= 4000000000; i++) {
if (i === 4000000000) {
self.postMessage(i)
}
}
}
main.js 接收信息
主线程接收信息也是一样的,用onmessage方法即可:
worker.onmessage = function (e) {
alert(`Reached ${e.data}`)
}
这里的形参e,也是一个MessageEvent对象。
这样就完成了。
完整的结构如下
// main.js
let boxes = document.querySelectorAll('.box')
let btn = document.getElementById('btn')
let count = document.getElementById('count')
let isOriginalColor = true
-------------------------------------------------
let worker = new Worker('worker.js') // 核心逻辑
-------------------------------------------------
btn.addEventListener('click', changeColor)
function changeColor() {
boxes.forEach(box => {
if (isOriginalColor) {
box.style.backgroundColor = 'rgb(22,87,172)'
} else {
box.style.backgroundColor = 'rgb(22,172,172)'
}
})
isOriginalColor = !isOriginalColor
}
-------------------------------------------------------
count.addEventListener('click', () => {
worker.postMessage('Start') // 点击按钮后才发送信息
})
worker.onmessage = function (e) { // 核心逻辑
console.log(e);
alert(`Reached ${e.data}`)
}
------------------------------------------------------------
// worker.js
self.onmessage = function (e) {
console.log(e);
for (let i = 0; i <= 4000000000; i++) {
if (i === 4000000000) {
self.postMessage(i)
}
}
}
注意咯,你如果想让文件正确运行,应当启用Live Server。
Web Worker 不能用的API
Web Worker是有限制的,某些API它可以用,但某些不能用:
1. DOM 相关(完全不可用)
| API | 原因 |
|---|---|
document | Worker 无 DOM 访问权限 |
window | Worker 的全局对象是 self |
element.style | 无法直接修改 DOM 样式 |
alert() / confirm() | 依赖 window,Worker 不能弹窗 |
localStorage / sessionStorage | 同步存储,Worker 只能用异步存储(如 IndexedDB) |
2. 部分 BOM(浏览器对象模型)API
| API | 替代方案 |
|---|---|
XMLHttpRequest | 改用 fetch()(Worker 支持) |
location.reload() | 无法直接操作页面导航 |
window.open() | Worker 不能打开新窗口 |
3. 其他限制
- 同步 API(如
fs.readFileSync,Node.js 特有,浏览器 Worker 不涉及) - 某些 WebGL 操作(部分高级
WebGLRenderingContext方法受限)
Web Worker 能用的 API
1. 网络请求
| API | 说明 |
|---|---|
fetch() | 发起 HTTP 请求(最常用) |
WebSocket | 建立长连接通信 |
2. 数据存储(异步)
| API | 说明 |
|---|---|
IndexedDB | 浏览器数据库存储 |
Cache API | Service Worker 专用缓存 |
3. 工具类 API
| API | 说明 |
|---|---|
setTimeout / setInterval | 定时器(回调在 Worker 线程执行) |
navigator(部分属性) | 如 navigator.userAgent |
console.log() | 可打印日志(但看不到 UI 控制台) |
Blob / File | 处理二进制数据 |
WebAssembly | 运行高性能编译代码 |
4. 线程通信
| API | 说明 |
|---|---|
postMessage() | 与主线程或其他 Worker 通信 |
BroadcastChannel | 跨 Worker/Tab 广播消息 |
SharedArrayBuffer | 共享内存(需谨慎使用) |
总结
OK,这一期就是这样了,让我们来稍微总结一下流程吧!
这里可能大家有些疑问,比如 “为什么你不在worker中直接操作alert呢?”
这是因为worker的执行环境和我们主线程JS执行环境不一样:
主线程执行全局环境为Window,而worker执行全局环境为self,这就决定了它不能够利用浏览器的一些API,不能操作DOM。