无阻塞UI:通过Web Worker提升用户体验的新途径
1.Web Worker 的定义
Web Worker 是浏览器提供的 JavaScript 多线程解决方案,允许在主线程之外运行脚本,执行耗时任务而不阻塞用户界面, 适用于处理耗时任务,避免阻塞主线程,提升用户体验。通过 postMessage
和 onmessage
实现主线程与 Worker 的通信.
2.使用场景
-
复杂计算:如图像处理、数据加密等。
-
大数据处理:如排序、过滤等。
-
实时通信:如 WebSocket 或轮询服务器。
-
后台任务:如日志上传、数据同步。
3.语法
1. 创建 Web Worker
// 主线程
const worker = new Worker('worker.js');
2. 主线程与 Worker 通信
- 主线程发送消息:
worker.postMessage('Hello Worker');
- 主线程接收消息:
worker.onmessage = function(event) { console.log('Received from worker:', event.data); };
3. Worker 脚本
- 接收消息:
self.onmessage = function(event) { console.log('Received from main thread:', event.data); };
- 发送消息:
self.postMessage('Hello Main Thread');
4. 终止 Worker
worker.terminate();
4.主要API
Web Worker 除了基本的 postMessage
和 onmessage
之外,还提供了其他一些 API 和方法,用于更灵活地控制 Worker 的行为和通信。以下是一些常用的 Web Worker API 和方法:
1. Worker 构造函数 ★ 常用
用于创建一个新的 Web Worker。
const worker = new Worker('worker.js');
2. postMessage ★ 常用
用于向 Worker 发送消息。
worker.postMessage(data);
// data 可以是任意类型的数据(如字符串、对象、数组等)。
// 数据会被结构化克隆算法(Structured Clone Algorithm)序列化后传递给 Worker。
3. onmessage ★ 常用
用于监听 Worker 发送的消息。
worker.onmessage = function(event) {
console.log('Received from worker:', event.data);
};
// event.data 是 Worker 发送的数据。
4. onerror ★ 常用
用于监听 Worker 中的错误。
worker.onerror = function(error) {
console.error('Worker error:', error);
};
// error 是一个 ErrorEvent 对象,包含错误的详细信息。
5. terminate ★ 常用
用于立即终止 Worker。
worker.terminate(); // 终止后,Worker 会停止运行,无法再接收或发送消息。
6. importScripts
在 Worker 中动态加载外部脚本。
// worker.js
importScripts('script1.js', 'script2.js');
// 可以加载多个脚本,脚本会按顺序执行。
// 加载的脚本必须是同源的。
7. close ★ 常用
在 Worker 内部调用,用于关闭 Worker。
// worker.js
self.close();
// Worker 会立即停止运行,无法再接收或发送消息
8. MessageChannel
用于在主线程和 Worker 之间创建双向通信通道。
// 主线程
const channel = new MessageChannel();
// 发送端口给 Worker
worker.postMessage({ port: channel.port1 }, [channel.port1]);
// 监听消息
channel.port2.onmessage = function(event) {
console.log('Received from worker:', event.data);
};
// 发送消息
channel.port2.postMessage('Hello Worker');
// worker.js
self.onmessage = function(event) {
const port = event.ports[0];
// 监听消息
port.onmessage = function(event) {
console.log('Received from main thread:', event.data);
};
// 发送消息
port.postMessage('Hello Main Thread');
};
9. BroadcastChannel
用于在多个 Worker 或主线程之间广播消息。
// 主线程或 Worker
const channel = new BroadcastChannel('my-channel');
// 发送消息
channel.postMessage('Hello everyone!');
// 监听消息
channel.onmessage = function(event) {
console.log('Received:', event.data);
};
10. SharedArrayBuffer 和 Atomics
用于在主线程和 Worker 之间共享内存。
// 主线程
const sharedBuffer = new SharedArrayBuffer(1024);
const sharedArray = new Int32Array(sharedBuffer);
// 发送共享内存给 Worker
worker.postMessage(sharedBuffer);
// 修改共享内存
Atomics.store(sharedArray, 0, 123);
// worker.js
self.onmessage = function(event) {
const sharedBuffer = event.data;
const sharedArray = new Int32Array(sharedBuffer);
// 读取共享内存
const value = Atomics.load(sharedArray, 0);
console.log('Shared value:', value);
};
11. Worker 生命周期事件 ★ 重要
Worker 支持以下生命周期事件:
-
onmessage:接收消息。
-
onerror:捕获错误。
-
onmessageerror:当接收到的消息无法反序列化时触发。
worker.onmessageerror = function(event) { console.error('Message error:', event); };
12. Worker 全局作用域
在 Worker 中,self
指向 Worker 的全局作用域。常用的属性和方法包括:
-
self.postMessage
:发送消息。 -
self.onmessage
:监听消息。 -
self.close
:关闭 Worker。 -
self.importScripts
:加载外部脚本。
完整示例:使用 MessageChannel 实现双向通信
以下是一个完整示例,展示如何使用 MessageChannel
在主线程和 Worker 之间实现双向通信。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Worker - MessageChannel 示例</title>
</head>
<body>
<h1>Web Worker - MessageChannel 示例</h1>
<button id="sendMessage">发送消息</button>
<div id="output"></div>
<script>
const worker = new Worker('worker.js');
const channel = new MessageChannel();
// 发送端口给 Worker
worker.postMessage({ port: channel.port1 }, [channel.port1]);
// 监听 Worker 的消息
channel.port2.onmessage = function(event) {
document.getElementById('output').innerHTML = `收到 Worker 的消息: ${event.data}`;
};
// 发送消息给 Worker
document.getElementById('sendMessage').addEventListener('click', function() {
channel.port2.postMessage('Hello Worker');
});
</script>
</body>
</html>
worker.js
self.onmessage = function(event) {
const port = event.ports[0];
// 监听主线程的消息
port.onmessage = function(event) {
console.log('收到主线程的消息:', event.data);
port.postMessage('Hello Main Thread');
};
};
Web Worker 提供了丰富的 API 和方法,包括:
-
基本通信:
postMessage
和onmessage
。 -
错误处理:
onerror
和onmessageerror
。 -
双向通信:
MessageChannel
。 -
广播通信:
BroadcastChannel
。 -
共享内存:
SharedArrayBuffer
和Atomics
。
5.web Work语法示例
主线程
const worker = new Worker('worker.js');
worker.postMessage('Start');
worker.onmessage = function(event) {
console.log('From Worker:', event.data);
};
Worker 脚本 (worker.js
)
self.onmessage = function(event) {
console.log('From Main:', event.data);
self.postMessage('Working...');
};
6.注意事项
-
无 DOM 访问:Worker 不能直接操作 DOM。
-
同源限制:Worker 脚本必须与主线程同源。★ 重要
-
有限 API:Worker 只能使用部分浏览器 API,如
XMLHttpRequest
、fetch
等
7.复杂计算阻塞主线程示例 ★ 反例
-
主线程阻塞:
-
JavaScript 是单线程的,复杂计算任务会占用主线程,导致页面无法响应用户交互。
-
即使是一些简单的操作(如点击按钮、播放动画)也会被阻塞。
-
-
用户体验差:
-
页面卡顿会让用户感到不流畅,甚至误以为页面崩溃。
-
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>复杂计算阻塞主线程示例</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
#output {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
background-color: #f9f9f9;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
margin-right: 10px;
}
#animation {
width: 100px;
height: 100px;
background-color: red;
margin-top: 20px;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<h1>复杂计算阻塞主线程示例</h1>
<p>点击“开始计算”按钮,复杂计算任务将在主线程中运行,阻塞页面交互。</p>
<button id="startCalculation">开始计算</button>
<button id="interactiveButton">测试交互</button>
<div id="output"></div>
<div id="animation"></div>
<script>
// 复杂计算任务:计算斐波那契数列(递归实现,性能较差)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 启动计算
document.getElementById('startCalculation').addEventListener('click', function() {
document.getElementById('output').innerHTML = "计算中...";
const startTime = Date.now();
const result = fibonacci(45); // 计算斐波那契数列的第 45 项
const endTime = Date.now();
document.getElementById('output').innerHTML = `计算完成,结果是: ${result},耗时: ${endTime - startTime} 毫秒`;
});
// 测试页面交互
document.getElementById('interactiveButton').addEventListener('click', function() {
alert('页面交互正常!');
});
</script>
</body>
</html>
实际效果展示,差点给我电脑干崩啦
8.Web Worker 不阻塞交互示例 ★ 案例
-
复杂计算任务在 Web Worker 中运行,不会阻塞主线程。
-
在计算过程中,用户可以正常点击按钮、观看动画,页面交互流畅。
-
计算完成后,结果会显示在页面上。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Worker 示例 - 不阻塞交互</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
#output {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
background-color: #f9f9f9;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
margin-right: 10px;
}
#animation {
width: 100px;
height: 100px;
background-color: red;
margin-top: 20px;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<h1>Web Worker 示例 - 不阻塞交互</h1>
<p>点击“开始计算”按钮,Web Worker 会在后台执行复杂计算,同时页面交互(如按钮点击和动画)不会受影响。</p>
<button id="startWorker">开始计算</button>
<button id="interactiveButton">测试交互</button>
<div id="output"></div>
<div id="animation"></div>
<script>
// 主线程代码
let worker;
// 启动 Worker
document.getElementById('startWorker').addEventListener('click', function() {
if (typeof(Worker) !== "undefined") {
if (!worker) {
worker = new Worker('worker.js'); // 创建 Web Worker
}
// 监听 Worker 发送的消息
worker.onmessage = function(event) {
document.getElementById('output').innerHTML = "计算完成,结果是: " + event.data;
};
// 向 Worker 发送消息,开始计算
worker.postMessage('开始计算');
document.getElementById('output').innerHTML = "计算中...";
} else {
document.getElementById('output').innerHTML = "抱歉,你的浏览器不支持 Web Worker。";
}
});
// 测试页面交互
document.getElementById('interactiveButton').addEventListener('click', function() {
alert('页面交互正常!');
});
</script>
<!-- Web Worker 脚本 -->
<script id="workerScript" type="javascript/worker">
// Worker 脚本
self.onmessage = function(event) {
if (event.data === '开始计算') {
// 模拟复杂计算(计算从 0 到 10 亿的和)
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
self.postMessage(result); // 将结果发送回主线程
}
};
</script>
<script>
// 将 Worker 脚本转换为 Blob URL
const blob = new Blob([document.getElementById('workerScript').textContent], { type: 'text/javascript' });
const workerUrl = URL.createObjectURL(blob);
// 替换 worker.js 的路径为 Blob URL
window.Worker = class extends Worker {
constructor() {
super(workerUrl);
}
};
</script>
</body>
</html>
实际效果展示,非常的流畅
9.拓展:为什么要用 Blob URL?★ 了解
-
无需外部文件:
-
通常情况下,Web Worker 需要从一个单独的 JavaScript 文件(如
worker.js
)加载脚本。 -
使用 Blob URL 可以将 Worker 脚本直接嵌入到 HTML 文件中,避免创建额外的文件。
-
-
方便演示和分享:
-
在示例代码或教程中,使用 Blob URL 可以让代码更简洁,用户只需复制一个 HTML 文件即可运行完整的功能。
-
不需要用户额外下载或创建
worker.js
文件。
-
-
动态生成脚本:
-
Blob URL 允许动态生成 Worker 脚本内容。例如,可以根据用户输入或其他条件动态修改 Worker 的逻辑。
-
-
同源策略:
-
Web Worker 的脚本文件必须与主页面同源(即相同的协议、域名和端口)。使用 Blob URL 可以避免跨域问题,因为 Blob URL 被视为同源。
-
10.拓展:Blob URL 的工作原理 ★ 了解
-
Blob:
-
Blob
是一个表示二进制数据的对象。我们可以将 JavaScript 代码作为字符串传递给Blob
,生成一个脚本文件。
-
-
URL.createObjectURL:
-
URL.createObjectURL
方法可以将Blob
对象转换为一个临时 URL。这个 URL 可以作为 Web Worker 的脚本路径。
-
-
动态加载:
-
通过将 Worker 脚本嵌入 HTML 中的
<script>
标签,然后提取其内容并转换为 Blob URL,最终动态加载 Worker 脚本。
-
Blob URL代码解析 ★ 了解
以下是代码中与 Blob URL 相关的部分:
<!-- Web Worker 脚本 -->
<script id="workerScript" type="javascript/worker">
// Worker 脚本
self.onmessage = function(event) {
if (event.data === '开始计算') {
// 模拟复杂计算(计算从 0 到 10 亿的和)
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += i;
}
self.postMessage(result); // 将结果发送回主线程
}
};
</script>
<script>
// 将 Worker 脚本转换为 Blob URL
const blob = new Blob([document.getElementById('workerScript').textContent], { type: 'text/javascript' });
const workerUrl = URL.createObjectURL(blob);
// 替换 worker.js 的路径为 Blob URL
window.Worker = class extends Worker {
constructor() {
super(workerUrl);
}
};
</script>
-
嵌入 Worker 脚本:
-
使用
<script id="workerScript" type="javascript/worker">
将 Worker 脚本嵌入 HTML 中。 -
type="javascript/worker"
是为了避免浏览器直接执行这段脚本。
-
-
提取脚本内容:
-
通过
document.getElementById('workerScript').textContent
获取嵌入的脚本内容。
-
-
生成 Blob URL:
-
将脚本内容传递给
Blob
构造函数,生成一个 Blob 对象。 -
使用
URL.createObjectURL(blob)
将 Blob 对象转换为一个临时 URL。
-
-
动态加载 Worker:
-
通过重写
window.Worker
,将 Blob URL 作为 Worker 的脚本路径。
-
优点
-
简化代码结构:无需额外文件,所有代码都在一个 HTML 文件中。
-
方便测试和演示:用户只需复制一个文件即可运行。
-
动态生成脚本:可以根据需要动态修改 Worker 的逻辑。
缺点
-
临时 URL:Blob URL 是临时的,页面刷新后会失效。
-
不适合大型项目:对于大型项目,建议将 Worker 脚本放在单独的文件中,便于维护和调试。
11.最佳实战写法 ★ 非常重要
在实际工作中,我们通常会使用单独的 worker.js
文件来编写 Web Worker 的逻辑,而不是使用 Blob URL。这样可以更好地组织代码,便于维护和调试
以下是一个完整的示例,展示如何使用单独的 worker.js
文件,并在其中执行复杂的计算任务(如矩阵乘法),然后将结果返回给主线程。同时,页面中会有一些交互(如按钮点击和动画),以展示 Web Worker 如何避免阻塞主线程
/project
├── index.html
├── worker.js
1. index.html
文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Worker 示例 - 实际工作场景</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
#output {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
background-color: #f9f9f9;
}
button {
padding: 10px 20px;
font-size: 16px;
cursor: pointer;
margin-right: 10px;
}
#animation {
width: 100px;
height: 100px;
background-color: red;
margin-top: 20px;
animation: rotate 2s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
</head>
<body>
<h1>Web Worker 示例 - 实际工作场景</h1>
<p>点击“开始计算”按钮,复杂计算任务将在 Web Worker 中运行,页面交互不受影响。</p>
<button id="startWorker">开始计算</button>
<button id="interactiveButton">测试交互</button>
<div id="output"></div>
<div id="animation"></div>
<script>
// 主线程代码
let worker;
// 启动 Worker
document.getElementById('startWorker').addEventListener('click', function() {
if (typeof(Worker) !== "undefined") {
if (!worker) {
worker = new Worker('worker.js'); // 创建 Web Worker
}
// 监听 Worker 发送的消息
worker.onmessage = function(event) {
const { result, time } = event.data;
document.getElementById('output').innerHTML = `计算完成,结果是: ${result},耗时: ${time} 毫秒`;
};
// 向 Worker 发送消息,开始计算
worker.postMessage({ action: 'start' });
document.getElementById('output').innerHTML = "计算中...";
} else {
document.getElementById('output').innerHTML = "抱歉,你的浏览器不支持 Web Worker。";
}
});
// 测试页面交互
document.getElementById('interactiveButton').addEventListener('click', function() {
alert('页面交互正常!');
});
// 终止 Worker(可选)
window.addEventListener('beforeunload', function() {
if (worker) {
worker.terminate(); // 关闭 Worker
}
});
</script>
</body>
</html>
2. worker.js
文件
// Worker 脚本
function matrixMultiplication(size) {
// 创建两个随机矩阵
const matrixA = Array.from({ length: size }, () =>
Array.from({ length: size }, () => Math.random() * 100)
);
const matrixB = Array.from({ length: size }, () =>
Array.from({ length: size }, () => Math.random() * 100)
);
// 结果矩阵
const result = Array.from({ length: size }, () =>
Array.from({ length: size }, () => 0)
);
// 矩阵乘法
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
for (let k = 0; k < size; k++) {
result[i][j] += matrixA[i][k] * matrixB[k][j];
}
}
}
return result;
}
self.onmessage = function(event) {
if (event.data.action === 'start') {
const startTime = Date.now();
const result = matrixMultiplication(200); // 计算 200x200 矩阵乘法
const endTime = Date.now();
self.postMessage({ result: '矩阵乘法完成', time: endTime - startTime }); // 将结果发送回主线程
}
};
效果展示
代码说明
-
复杂计算任务:
-
在
worker.js
中,模拟了一个复杂的计算任务:矩阵乘法。 -
创建两个 200x200 的随机矩阵,并计算它们的乘积。这个任务非常耗时,可以明显感受到性能差异。
-
-
主线程与 Worker 通信:
-
主线程通过
worker.postMessage
向 Worker 发送消息,启动计算任务。 -
Worker 通过
self.postMessage
将计算结果返回给主线程。
-
-
页面交互:
-
点击“测试交互”按钮时,会弹出一个提示框。
-
页面中还有一个红色方块的旋转动画,用于直观展示页面是否卡顿。
-
-
终止 Worker:
-
在页面关闭时,通过
worker.terminate()
终止 Worker,释放资源。
-
运行效果
-
打开浏览器,运行
index.html
。 -
点击“开始计算”按钮,Web Worker 会在后台执行矩阵乘法任务。
-
在计算过程中:
-
点击“测试交互”按钮,页面会立即弹出提示框,证明页面交互未被阻塞。
-
红色方块的旋转动画会一直流畅运行,证明主线程未被阻塞。
-
-
计算完成后,结果会显示在页面上。
总结
-
实际工作场景:使用单独的
worker.js
文件编写 Web Worker 逻辑,便于维护和调试。 -
复杂计算任务:通过矩阵乘法模拟复杂计算任务,展示 Web Worker 的性能优势。
-
返回数据给主线程:Worker 可以通过
postMessage
将计算结果返回给主线程。