第9章 9.3 Web Worker 与多线程

📌 上一章我们学会了用 WebAssembly 把 C/C++ 代码跑在浏览器里,性能飞起!但你有没有发现一个问题——无论是 JavaScript 还是 WebAssembly,它们都在同一个线程里干活。就像只有一个厨师的厨房,再厉害的大厨也会被切菜、炒菜、摆盘这些事情搞得手忙脚乱。这一章,我们要给浏览器厨房多加几个厨师——让不同的任务真正同时执行。

你有没有遇到过这种情况:网页加载一个复杂的数据处理,结果浏览器直接卡死了,点什么都没反应。这就是因为 JavaScript 默认的单线程机制——所有计算都在 UI 线程里跑,一旦遇到耗时的任务,页面就像死机一样。

学完这章,你能:用 Web Worker 创建后台线程,让耗时计算不阻塞页面,让你的网页保持流畅


🎯 开场 3 分钟:为什么要学这个?

生活中的排队场景

想象你去银行办业务
- 只有一个窗口时,大叔在填表,后面的人全部等着
- 开多个窗口后,大叔填表不影响别人开户

浏览器也一\n\nSimple tech illustration expla\n\nAI comic creation scene, creat\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,也不能访问页面的一些对象(如 windowdocument)。它是独立的运行环境。


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 双向通信,让页面保持流畅响应。

延伸学习

  1. MDN Web Docs - Web Workershttps://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API(官方文档,最权威)
  2. 《JavaScript 高级程序设计》:第 22 章,深入讲解 Worker 和性能优化
  3. Google Developers - Worker Performancehttps://developers.google.com/web/updates/2018/08/workers-filament(官方性能指南)

互动钩子

🧵 你在做项目时遇到过页面卡死的问题吗?当时是怎么解决的?评论区聊聊你的"卡死血泪史",老粉优先回复!


📌 下章预告:写完了多线程处理,代码跑起来了,但你有没有想过——怎么确保代码是对的? 万一 Worker 返回的数据是错的怎么办?下一章我们要学习自动化测试,用 Jest 给你的代码装上"质检关卡"!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。