跳到主要内容

Web Workers

问题

什么是 Web Worker?如何在浏览器中实现多线程?Service Worker 是什么?

答案

Web Workers 允许在后台线程中运行 JavaScript,不会阻塞主线程(UI 线程)。适用于 CPU 密集型任务。

Worker 类型概览

类型作用域生命周期使用场景
Web Worker单页面页面关闭时销毁复杂计算
Shared Worker多页面共享所有连接关闭时销毁跨标签页通信
Service Worker整个源独立于页面离线缓存、推送通知

Web Worker

基本用法

// main.ts - 主线程
const worker = new Worker(new URL('./worker.ts', import.meta.url));

// 发送消息
worker.postMessage({ type: 'calculate', data: [1, 2, 3, 4, 5] });

// 接收消息
worker.onmessage = (e: MessageEvent) => {
console.log('结果:', e.data);
};

// 错误处理
worker.onerror = (e: ErrorEvent) => {
console.error('Worker 错误:', e.message);
};

// 终止 Worker
worker.terminate();
// worker.ts - Worker 线程
self.onmessage = (e: MessageEvent) => {
const { type, data } = e.data;

if (type === 'calculate') {
// 执行耗时计算
const result = data.reduce((sum: number, n: number) => sum + n, 0);

// 返回结果
self.postMessage({ type: 'result', value: result });
}
};

// Worker 中可用的 API
// ✅ setTimeout, setInterval
// ✅ fetch, XMLHttpRequest
// ✅ IndexedDB
// ✅ WebSocket
// ❌ DOM 操作
// ❌ window 对象
// ❌ document 对象

Worker 限制

可用不可用
setTimeout / setIntervalwindow
fetch / XMLHttpRequestdocument
IndexedDBDOM 操作
WebSocketalert / confirm
importScripts()localStorage
crypto父页面变量

传递数据

// 1. 结构化克隆(默认)- 深拷贝
worker.postMessage({ data: largeArray }); // 数据被复制

// 2. Transferable Objects - 转移所有权(零拷贝)
const buffer = new ArrayBuffer(1024 * 1024); // 1MB
worker.postMessage(buffer, [buffer]); // 转移,主线程无法再访问

console.log(buffer.byteLength); // 0,已转移

// 支持转移的类型
// ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas

封装 Worker 通信

// 类型安全的 Worker 封装
interface WorkerMessage {
id: number;
type: string;
payload: any;
}

interface WorkerResponse {
id: number;
result?: any;
error?: string;
}

function createWorkerAPI<T extends Record<string, (...args: any[]) => any>>(
workerUrl: URL
): T {
const worker = new Worker(workerUrl);
const pending = new Map<number, { resolve: Function; reject: Function }>();
let messageId = 0;

worker.onmessage = (e: MessageEvent<WorkerResponse>) => {
const { id, result, error } = e.data;
const handlers = pending.get(id);
if (handlers) {
pending.delete(id);
if (error) {
handlers.reject(new Error(error));
} else {
handlers.resolve(result);
}
}
};

return new Proxy({} as T, {
get(_, method: string) {
return (...args: any[]) => {
return new Promise((resolve, reject) => {
const id = messageId++;
pending.set(id, { resolve, reject });
worker.postMessage({ id, type: method, payload: args });
});
};
},
});
}

// 使用
interface MathWorker {
add: (a: number, b: number) => Promise<number>;
fibonacci: (n: number) => Promise<number>;
}

const math = createWorkerAPI<MathWorker>(
new URL('./math.worker.ts', import.meta.url)
);

const result = await math.add(1, 2); // 3

实际应用示例

// 图片处理 Worker
// imageWorker.ts
self.onmessage = async (e: MessageEvent) => {
const { imageData, filter } = e.data;

// 在 Worker 中处理图片数据
const processed = applyFilter(imageData, filter);

// 使用 Transferable 返回
self.postMessage(processed, [processed.data.buffer]);
};

function applyFilter(imageData: ImageData, filter: string): ImageData {
const data = imageData.data;

switch (filter) {
case 'grayscale':
for (let i = 0; i < data.length; i += 4) {
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
data[i] = data[i + 1] = data[i + 2] = avg;
}
break;
case 'invert':
for (let i = 0; i < data.length; i += 4) {
data[i] = 255 - data[i];
data[i + 1] = 255 - data[i + 1];
data[i + 2] = 255 - data[i + 2];
}
break;
}

return imageData;
}

Shared Worker

Shared Worker 可以被多个页面(同源)共享:

// main.ts - 任意页面
const sharedWorker = new SharedWorker(
new URL('./shared.worker.ts', import.meta.url)
);

// 通过 port 通信
sharedWorker.port.start();

sharedWorker.port.postMessage({ type: 'join', name: 'Page1' });

sharedWorker.port.onmessage = (e: MessageEvent) => {
console.log('收到:', e.data);
};
// shared.worker.ts
const connections: MessagePort[] = [];

self.onconnect = (e: MessageEvent) => {
const port = e.ports[0];
connections.push(port);

port.onmessage = (e: MessageEvent) => {
// 广播消息给所有连接
connections.forEach(p => {
p.postMessage({
from: e.data.name,
message: e.data.message,
connections: connections.length,
});
});
};

port.start();
};

使用场景

  • 跨标签页状态同步
  • WebSocket 连接共享
  • 共享缓存

Service Worker

Service Worker 是一个网络代理,可以拦截页面的网络请求,实现离线缓存、推送通知等功能。

生命周期

注册 Service Worker

// main.ts
if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => {
try {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/', // 控制范围
});

console.log('SW 注册成功:', registration.scope);

// 监听更新
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing;
newWorker?.addEventListener('statechange', () => {
if (newWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
console.log('新版本可用');
}
}
});
});
} catch (error) {
console.error('SW 注册失败:', error);
}
});
}

缓存策略

// sw.ts
const CACHE_NAME = 'v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles.css',
'/app.js',
];

// 安装时缓存静态资源
self.addEventListener('install', (event: ExtendableEvent) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
return cache.addAll(STATIC_ASSETS);
})
);
// 跳过等待,立即激活
self.skipWaiting();
});

// 激活时清理旧缓存
self.addEventListener('activate', (event: ExtendableEvent) => {
event.waitUntil(
caches.keys().then(keys => {
return Promise.all(
keys
.filter(key => key !== CACHE_NAME)
.map(key => caches.delete(key))
);
})
);
// 接管所有客户端
self.clients.claim();
});

// 拦截请求
self.addEventListener('fetch', (event: FetchEvent) => {
event.respondWith(
cacheFirst(event.request)
);
});

常见缓存策略

// 1. Cache First(缓存优先)
async function cacheFirst(request: Request): Promise<Response> {
const cached = await caches.match(request);
if (cached) return cached;

const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
}

// 2. Network First(网络优先)
async function networkFirst(request: Request): Promise<Response> {
try {
const response = await fetch(request);
const cache = await caches.open(CACHE_NAME);
cache.put(request, response.clone());
return response;
} catch {
const cached = await caches.match(request);
if (cached) return cached;
throw new Error('No cache available');
}
}

// 3. Stale While Revalidate(返回缓存,后台更新)
async function staleWhileRevalidate(request: Request): Promise<Response> {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);

const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});

return cached || fetchPromise;
}

// 4. Network Only(仅网络)
async function networkOnly(request: Request): Promise<Response> {
return fetch(request);
}

// 5. Cache Only(仅缓存)
async function cacheOnly(request: Request): Promise<Response> {
const cached = await caches.match(request);
if (cached) return cached;
throw new Error('Not in cache');
}
策略说明适用场景
Cache First优先缓存静态资源
Network First优先网络API 请求
Stale While Revalidate返回缓存,后台更新频繁更新的资源
Network Only仅网络实时数据
Cache Only仅缓存离线页面

后台同步

// sw.ts
self.addEventListener('sync', (event: SyncEvent) => {
if (event.tag === 'sync-messages') {
event.waitUntil(syncMessages());
}
});

async function syncMessages() {
const messages = await getUnsentMessages();
for (const msg of messages) {
await sendMessage(msg);
await markAsSent(msg.id);
}
}

// main.ts - 注册同步
async function saveMessage(message: string) {
await saveToIndexedDB(message);

const registration = await navigator.serviceWorker.ready;
await registration.sync.register('sync-messages');
}

常见面试问题

Q1: Web Worker 和主线程如何通信?

答案

通过 postMessageonmessage 进行通信:

// 主线程
worker.postMessage(data);
worker.onmessage = (e) => console.log(e.data);

// Worker 线程
self.onmessage = (e) => console.log(e.data);
self.postMessage(result);

数据传递方式

  1. 结构化克隆(默认):深拷贝数据
  2. Transferable:转移所有权,零拷贝
// 转移 ArrayBuffer
worker.postMessage(buffer, [buffer]);

Q2: Web Worker、Shared Worker、Service Worker 的区别?

答案

特性Web WorkerShared WorkerService Worker
作用域单页面多页面共享整个源
生命周期页面关闭销毁连接关闭销毁独立于页面
主要用途CPU 密集计算跨页面通信离线缓存、推送
可访问 DOM
可拦截请求

Q3: Service Worker 有哪些常见的缓存策略?

答案

策略优先级使用场景
Cache First缓存 > 网络静态资源(CSS/JS/图片)
Network First网络 > 缓存API 请求、经常变化的数据
Stale While Revalidate缓存,后台更新需要快速响应但内容可能变化

Q4: 为什么 Worker 不能操作 DOM?

答案

DOM 操作不是线程安全的。如果多个线程同时操作 DOM,会导致竞态条件和不可预测的结果。

解决方案

  1. Worker 只做计算,返回结果给主线程
  2. 主线程负责 DOM 操作
  3. 如果需要操作 Canvas,可以使用 OffscreenCanvas
// 使用 OffscreenCanvas
const offscreen = canvas.transferControlToOffscreen();
worker.postMessage({ canvas: offscreen }, [offscreen]);

Q5: 如何使用 Service Worker 实现离线应用?

答案

// 1. 注册 Service Worker
navigator.serviceWorker.register('/sw.js');

// 2. 安装时缓存资源
self.addEventListener('install', (e) => {
e.waitUntil(
caches.open('v1').then(cache => cache.addAll([
'/', '/index.html', '/app.js', '/styles.css'
]))
);
});

// 3. 拦截请求,返回缓存
self.addEventListener('fetch', (e) => {
e.respondWith(
caches.match(e.request).then(cached => cached || fetch(e.request))
);
});

相关链接