第9章 9.3 Web Worker 与多线程
📌 上一章我们学会了用 WebAssembly 把 C/C++ 代码跑在浏览器里,性能飞起!但你有没有发现一个问题——无论是 JavaScript 还是 WebAssembly,它们都在同一个线程里干活。就像只有一个厨师的厨房,再厉害的大厨也会被切菜、炒菜、摆盘这些事情搞得手忙脚乱。这一章,我们要给浏览器厨房多加几个厨师——让不同的任务真正同时执行。
你有没有遇到过这种情况:网页加载一个复杂的数据处理,结果浏览器直接卡死了,点什么都没反应。这就是因为 JavaScript 默认的单线程机制——所有计算都在 UI 线程里跑,一旦遇到耗时的任务,页面就像死机一样。
学完这章,你能:用 Web Worker 创建后台线程,让耗时计算不阻塞页面,让你的网页保持流畅。
🎯 开场 3 分钟:为什么要学这个?
生活中的排队场景
想象你去银行办业务:
- 只有一个窗口时,大叔在填表,后面的人全部等着
- 开多个窗口后,大叔填表不影响别人开户
浏览器也一\n\n
\n\n
\n\n样:默认只有一个"窗口"(主线程),所有事情都得排队。加载大数据、加密解密、图片处理……只要一个任务卡住,整个页面都卡。
你的痛,老外也有
来看看真实场景:
// 假设你要做一个在线图片处理工具
// 处理一张高清图片要 3 秒,页面直接卡死 3 秒
function processImage(imageData) {
// 复杂计算...
for (let i = 0; i < imageData.length; i++) {
// 像素处理...
}
return processedImage;
}
// 点完"处理"按钮,用户想点"取消"——抱歉,点不了,因为线程被占用了
document.getElementById('processBtn').addEventListener('click', () => {
processImage(hugeImageData); // 页面卡死 3 秒
});
学完这章能解决
- ✅ 耗时任务在后台跑,页面不卡
- ✅ 用户可以随时取消或继续操作
- ✅ 充分利用多核 CPU(现在手机都是 8 核的!)
- ✅ 实现真正的并行处理
💡 等等,Python 呢? 说了半天 JavaScript,但 Python 其实也有类似场景——跑机器学习模型时 UI 也会卡。这一章我们用 JavaScript 的 Web Worker 讲透多线程思维,这些思想在 Python 里完全通用。
🧱 基础 25 分钟:核心概念
9.3.1 什么是 Web Worker?
是什么:Web Worker 是浏览器提供的一个独立线程环境,可以运行 JavaScript 代码而不阻塞主线程。
生活类比:想象餐厅里:
- 主线程 = 服务员,只负责接待客人、端菜、收钱
- Worker 线程 = 后厨厨师,专注做菜,不被客人打断
为什么要用:当你要做耗时计算(大数据处理、加密、图像处理),如果不放到 Worker 里,页面会直接卡死。
怎么用:
// main.js - 主线程
const worker = new Worker('worker.js'); // 创建 Worker
// 给 Worker 发消息
worker.postMessage({ type: 'start', data: [1, 2, 3, 4, 5] });
// 接收 Worker 的返回结果
worker.onmessage = function(e) {
console.log('Worker 返回的结果:', e.data); // 输出: [2, 4, 6, 8, 10]
};
worker.onerror = function(e) {
console.error('Worker 出错了:', e.message);
};
// worker.js - Worker 线程
// 接收主线程的消息
self.onmessage = function(e) {
const numbers = e.data.data;
// 模拟耗时计算:每个数翻倍
const result = numbers.map(n => n * 2);
// 把结果发回主线程
self.postMessage(result);
};
运行流程图:
⚠️ 注意:Worker 里不能直接操作 DOM,也不能访问页面的一些对象(如
window、document)。它是独立的运行环境。
9.3.2 postMessage 通信机制
是什么:postMessage 是 Worker 与主线程之间双向通信的机制,类似对讲机。
为什么要用:主线程和 Worker 在不同线程,不能直接调用对方的方法,必须通过消息传递。
怎么用:
// 主线程
const myWorker = new Worker('compute.js');
// 发送数据(可以是任何可序列化对象)
myWorker.postMessage({
operation: 'calculate',
payload: { a: 10, b: 20 }
});
// 接收返回
myWorker.onmessage = function(e) {
console.log('收到 Worker 的回复:', e.data);
// 输出: 收到 Worker 的回复: { result: 30, from: 'worker' }
};
// Worker 线程 (compute.js)
self.onmessage = function(e) {
const { operation, payload } = e.data;
if (operation === 'calculate') {
const sum = payload.a + payload.b;
self.postMessage({ result: sum, from: 'worker' });
}
};
消息格式约定(建议这样用):
// 推荐:用 type 字段区分不同消息
const message = {
type: 'CALCULATE', // 消息类型
requestId: 'req_001', // 请求 ID,方便追踪
data: { // 实际数据
numbers: [1, 2, 3, 4, 5]
}
};
myWorker.postMessage(message);
9.3.3 SharedWorker 共享 worker
是什么:SharedWorker 是可以被多个页面/标签页共享的 Worker。
为什么要用:比如你打开了同一个网站的多个标签页,它们需要共享一个连接(WebSocket、数据库等),用 SharedWorker 避免重复连接。
怎么用:
// shared_worker.js
const connections = new Map();
self.onconnect = function(e) {
const port = e.ports[0];
port.onmessage = function(e) {
const { type, tabId } = e.data;
if (type === 'register') {
// 记录这个标签页
connections.set(tabId, port);
port.postMessage({ type: 'registered', tabId });
}
};
port.start();
};
// main.js - 每个页面都这样用
const sharedWorker = new SharedWorker('shared_worker.js');
sharedWorker.port.onmessage = function(e) {
console.log('来自 SharedWorker:', e.data);
};
sharedWorker.port.postMessage({ type: 'register', tabId: 'tab_1' });
sharedWorker.port.start();
9.3.4 Worker 的生命周期
创建 Worker → 接收消息 → 处理任务 → 发送结果 → (可选) 关闭 Worker
↓
new Worker()
↓
worker.terminate() // 主线程主动关闭
↓
self.close() // Worker 内部自己关闭
// 主线程控制 Worker 生命周期
const worker = new Worker('task.js');
// 主动关闭
function cleanup() {
worker.terminate(); // 主线程关闭 Worker
}
// Worker 内部也可以自己关闭
// self.close(); // 相当于 return,不再处理新消息
🔥 实战 35 分钟:3 个递进的小项目
项目 1:5 分钟 - 数字翻倍器(理解核心 API)
场景:学习 Worker 最基础的用法——主线程发送数字,Worker 返回翻倍后的结果。
完整可运行代码:
// === index.html ===
<!DOCTYPE html>
<html>
<head>
<title>数字翻倍器</title>
</head>
<body>
<h1>数字翻倍器</h1>
<input type="text" id="inputNumbers" placeholder="输入数字,用逗号分隔,如:1,2,3">
<button id="doubleBtn">翻倍!</button>
<div id="result"></div>
<script>
// 创建 Worker
const worker = new Worker('double_worker.js');
document.getElementById('doubleBtn').addEventListener('click', () => {
const input = document.getElementById('inputNumbers').value;
const numbers = input.split(',').map(n => parseInt(n.trim()));
// 发送数据给 Worker
worker.postMessage(numbers);
});
// 接收 Worker 返回的结果
worker.onmessage = function(e) {
document.getElementById('result').textContent = '结果: ' + e.data.join(', ');
};
worker.onerror = function(e) {
console.error('出错了:', e.message);
};
</script>
</body>
</html>
// === double_worker.js ===
self.onmessage = function(e) {
const numbers = e.data;
const doubled = numbers.map(n => n * 2);
self.postMessage(doubled);
};
预期输出(输入 1, 2, 3, 4, 5 后点击按钮):
结果: 2, 4, 6, 8, 10
一句话解释:Worker 处理完数据后通过 postMessage 把结果发回主线程,主线程更新页面显示。
项目 2:15 分钟 - 异步数据处理器(带真实场景)
场景:做一个文章阅读统计工具,输入文章内容,Worker 在后台统计字数、词频、句子数。
完整可运行代码:
// === index.html ===
<!DOCTYPE html>
<html>
<head>
<title>文章阅读统计</title>
<style>
textarea { width: 400px; height: 150px; }
.stats { margin-top: 20px; }
.stats div { margin: 5px 0; }
</style>
</head>
<body>
<h1>📊 文章阅读统计</h1>
<textarea id="articleInput" placeholder="粘贴文章内容..."></textarea>
<br>
<button id="analyzeBtn">开始统计</button>
<div class="stats" id="statsResult"></div>
<script>
const worker = new Worker('analyzer_worker.js');
document.getElementById('analyzeBtn').addEventListener('click', () => {
const text = document.getElementById('articleInput').value;
if (!text.trim()) {
alert('请输入文章内容');
return;
}
// 显示加载状态
document.getElementById('statsResult').innerHTML = '正在分析...';
// 发送文章给 Worker
worker.postMessage({ text, requestId: Date.now() });
});
worker.onmessage = function(e) {
const { charCount, wordCount, sentenceCount, topWords } = e.data;
let html = '<h3>统计结果:</h3>';
html += `<div>📝 总字符数:${charCount}</div>`;
html += `<div>📖 总词数:${wordCount}</div>`;
html += `<div>📃 句子数:${sentenceCount}</div>`;
html += `<div>🔥 高频词 Top 5:${topWords.join(', ')}</div>`;
document.getElementById('statsResult').innerHTML = html;
};
</script>
</body>
</html>
// === analyzer_worker.js ===
self.onmessage = function(e) {
const { text } = e.data;
// 字符数(不含空格)
const charCount = text.replace(/\s/g, '').length;
// 词数
const words = text.trim().split(/\s+/);
const wordCount = words.length;
// 句子数
const sentences = text.split(/[.!?。!?]+/);
const sentenceCount = sentences.filter(s => s.trim().length > 0).length;
// 词频统计
const wordFreq = {};
words.forEach(word => {
const cleanWord = word.toLowerCase().replace(/[^a-z\u4e00-\u9fa5]/g, '');
if (cleanWord) {
wordFreq[cleanWord] = (wordFreq[cleanWord] || 0) + 1;
}
});
// 取 Top 5 高频词
const topWords = Object.entries(wordFreq)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([word]) => word);
// 返回结果
self.postMessage({ charCount, wordCount, sentenceCount, topWords });
};
预期输出(粘贴一段英文文章后点击按钮):
📝 总字符数:1234
📖 总词数:256
📃 句子数:12
🔥 高频词 Top 5:the, and, is, to, of
一句话解释:Worker 在后台完成了耗时的文本分析,主线程不受阻塞,用户界面保持响应。
项目 3:15 分钟 - 实时进度显示的图片处理器
场景:处理多张图片,每处理完一张就更新进度条,展示 Worker 与主线程的双向通信。
完整可运行代码:
// === index.html ===
<!DOCTYPE html>
<html>
<head>
<title>批量图片处理</title>
<style>
#progressBar { width: 300px; height: 20px; background: #eee; border-radius: 10px; }
#progressFill { width: 0%; height: 100%; background: #4CAF50; border-radius: 10px; transition: width 0.3s; }
#log { margin-top: 20px; font-size: 14px; color: #666; max-height: 150px; overflow-y: auto; }
</style>
</head>
<body>
<h1>🖼️ 批量图片处理</h1>
<button id="startBtn">处理 10 张"图片"</button>
<div id="progressBar"><div id="progressFill"></div></div>
<div id="progressText">等待开始...</div>
<div id="log"></div>
<script>
const worker = new Worker('image_processor_worker.js');
let completedCount = 0;
const totalImages = 10;
function log(message) {
const logDiv = document.getElementById('log');
logDiv.innerHTML += `<div>> ${message}</div>`;
logDiv.scrollTop = logDiv.scrollHeight;
}
document.getElementById('startBtn').addEventListener('click', () => {
completedCount = 0;
document.getElementById('progressFill').style.width = '0%';
document.getElementById('progressText').textContent = '处理中...';
document.getElementById('log').innerHTML = '';
log('开始批量处理...');
// 发送 10 个任务给 Worker
for (let i = 1; i <= totalImages; i++) {
worker.postMessage({
type: 'process',
imageId: `img_${i}`,
imageData: Array.from({ length: 10000 }, (_, idx) => idx * i) // 模拟图片数据
});
}
});
worker.onmessage = function(e) {
const { type, imageId, progress, result } = e.data;
if (type === 'progress') {
// 进度更新
completedCount = progress.completed;
const percent = Math.round((completedCount / totalImages) * 100);
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressText').textContent = `处理中... ${completedCount}/${totalImages}`;
log(`[${imageId}] 进度: ${progress.percent}%`);
} else if (type === 'complete') {
// 单张完成
log(`✅ ${imageId} 处理完成,耗时 ${result.processTime}ms`);
// 全部完成
if (completedCount === totalImages) {
document.getElementById('progressText').textContent = '全部处理完成!';
log('🎉 全部任务完成!');
}
}
};
worker.onerror = function(e) {
log(`❌ 出错了: ${e.message}`);
};
</script>
</body>
</html>
// === image_processor_worker.js ===
let completedCount = 0;
self.onmessage = function(e) {
const { type, imageId, imageData } = e.data;
if (type === 'process') {
const startTime = Date.now();
const totalSteps = 5;
// 模拟分步骤处理(每个步骤发送进度)
for (let step = 1; step <= totalSteps; step++) {
// 模拟耗时处理
const result = processStep(imageData, step);
// 发送进度
completedCount++;
self.postMessage({
type: 'progress',
imageId,
progress: {
completed: completedCount,
percent: Math.round((step / totalSteps) * 100)
}
});
}
const processTime = Date.now() - startTime;
// 发送完成消息
self.postMessage({
type: 'complete',
imageId,
result: {
processTime,
outputSize: result.length
}
});
}
};
function processStep(data, step) {
// 模拟每一步的处理(做一些计算)
let result = data;
for (let i = 0; i < 1000; i++) {
result = result.map(v => (v * 31 + step) % 1000000);
}
return result;
}
预期输出(点击按钮后):
> 开始批量处理...
> [img_1] 进度: 20%
> [img_2] 进度: 40%
> ✅ img_1 处理完成,耗时 45ms
> [img_3] 进度: 60%
> ✅ img_2 处理完成,耗时 43ms
> ...
> 全部处理完成!
> 🎉 全部任务完成!
一句话解释:Worker 通过不同的 type 区分进度更新和完成通知,主线程根据消息类型更新 UI,实现了实时反馈的并行处理。
💪 进阶 20 分钟:常见坑 + 性能小贴士
❌ 坑 1:Worker 里不能访问 DOM
// ❌ 错误:在 Worker 里操作 DOM
self.onmessage = function(e) {
document.getElementById('result').textContent = e.data; // 会报错!
};
// ✅ 正确:Worker 只负责计算,通过 postMessage 返回结果
self.onmessage = function(e) {
const result = calculate(e.data);
self.postMessage(result); // 主线程收到后再更新 DOM
};
❌ 坑 2:postMessage 传递对象是拷贝不是引用
// ❌ 主线程:以为 Worker 修改了 myData 自己也会变
const myData = { value: 1 };
worker.postMessage(myData);
// Worker 里:self.postMessage({ ...e.data, value: 2 });
// 主线程:myData.value 还是 1!(除非收到 Worker 的回复)
console.log(myData.value); // 1,不是 2
// ✅ 正确:用 Transferable 对象转移所有权(适合大数据)
const buffer = new ArrayBuffer(100);
worker.postMessage(buffer, [buffer]); // 转移后主线程不能访问这个 buffer
❌ 坑 3:忘了 Worker 是异步的
// ❌ 错误:以为 Worker 会立即返回结果
const worker = new Worker('compute.js');
worker.postMessage({ type: 'heavy', data: bigArray });
console.log(result); // 此时 result 还是 undefined,主线程继续执行
// ✅ 正确:用回调或 Promise 包装
function computeInWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker('compute.js');
worker.postMessage({ data });
worker.onmessage = (e) => resolve(e.data);
worker.onerror = (e) => reject(e.message);
});
}
// 使用
async function main() {
const result = await computeInWorker(bigArray);
console.log(result); // 现在有值了
}
❌ 坑 4:Worker URL 不能跨域(除非服务器允许)
// ❌ 错误:从 CDN 加载 Worker
const worker = new Worker('https://cdn.example.com/worker.js'); // 可能被阻止
// ✅ 正确:1. 用同源文件 2. 或用 Blob URL
const workerCode = `
self.onmessage = function(e) {
self.postMessage(e.data * 2);
};
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
const worker = new Worker(workerUrl);
❌ 坑 5:创建太多 Worker 反而变慢
// ❌ 错误:每个任务都新建 Worker(创建/销毁开销大)
for (let task of tasks) {
const worker = new Worker('task.js'); // 创建 Worker 很慢!
worker.postMessage(task);
}
// ✅ 正确:用 MessageChannel 或维护 Worker 池
class WorkerPool {
constructor(workerPath, poolSize) {
this.workers = [];
for (let i = 0; i < poolSize; i++) {
this.workers.push(new Worker(workerPath));
}
}
// 复用已有的 Worker
}
💡 性能小贴士:用 SharedWorker 共享连接
如果多个标签页都要连同一个 WebSocket,用 SharedWorker 只建立一条连接:
// shared_worker.js
let socket = null;
const ports = [];
self.onconnect = function(e) {
const port = e.ports[0];
ports.push(port);
port.start();
if (!socket) {
// 只有第一个连接时创建 socket
socket = new WebSocket('wss://api.example.com');
socket.onmessage = (e) => {
// 广播给所有标签页
ports.forEach(p => p.postMessage({ type: 'message', data: e.data }));
};
}
};
🔧 调试技巧:Worker 里也能用 console
// worker.js
self.onmessage = function(e) {
console.log('Worker 收到:', e.data); // 会在浏览器控制台显示
// 试试 debugger
// debugger;
self.postMessage(result);
};
💡 Chrome 调试:打开 DevTools → Sources → 左侧可以看到 Worker 文件,像调试普通 JS 一样加断点。
✏️ 练习题
练习 1(2 分钟):换个运算
- 输入:在项目 1 中,把
n * 2改成n + 100 - 预期输出:
101, 102, 103, 104, 105 - 提示:只改 worker 文件里的一行
练习 2(2 分钟):加个条件判断
- 输入:在项目 1 的 Worker 里,只对大于 3 的数字翻倍
- 预期输出(输入
1,2,3,4,5):1, 2, 3, 8, 10 - 提示:加一个
if判断
练习 3(3 分钟):统计新数据
- 输入:用项目 2 的代码,统计任意一段中文文章
- 预期输出:字数、词数、句子数、高频词
- 提示:代码已经支持中文,无需修改
练习 4(5 分钟):串项目 1 和项目 2
- 输入:让 Worker 先做数字翻倍(项目 1),再统计结果(项目 2 的分析功能)
- 预期输出:翻倍后的数字的统计结果
- 提示:在一个 Worker 里依次调用两个处理函数
练习 5(3 分钟):读懂报错
- 输入:以下代码运行时报错
Uncaught DOMException: Failed to construct 'Worker': Script at 'file:///worker.js' could not be loaded - 原因:(请分析)
- 提示:浏览器不允许直接加载本地文件的 Worker,需要用
http://服务器
作业:做一个「多线程文件处理器」
需求描述:做一个网页工具,能处理用户输入的多段文本,展示 Worker 如何加速处理。
功能点:
1. 分段处理:把用户输入的文本分成 N 段,用 N 个 Worker 同时处理
2. 进度显示:每个 Worker 处理完一段就更新进度
3. 汇总结果:所有 Worker 完成后,汇总输出
加分项:
1. 支持手动调整 Worker 数量(1-8 个)
2. 显示每个 Worker 的处理耗时
验收标准:
- 能跑起来(用 http-server 或 Live Server)
- 输入文本后点击处理,进度条能实时更新
- 全部完成后显示汇总结果
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
一句话总结
这一章我们学会了用 Web Worker 把耗时任务放到后台线程执行,通过 postMessage 双向通信,让页面保持流畅响应。
延伸学习
- MDN Web Docs - Web Workers:
https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API(官方文档,最权威) - 《JavaScript 高级程序设计》:第 22 章,深入讲解 Worker 和性能优化
- Google Developers - Worker Performance:
https://developers.google.com/web/updates/2018/08/workers-filament(官方性能指南)
互动钩子
🧵 你在做项目时遇到过页面卡死的问题吗?当时是怎么解决的?评论区聊聊你的"卡死血泪史",老粉优先回复!
📌 下章预告:写完了多线程处理,代码跑起来了,但你有没有想过——怎么确保代码是对的? 万一 Worker 返回的数据是错的怎么办?下一章我们要学习自动化测试,用 Jest 给你的代码装上"质检关卡"!

评论(0)