Web Workers

Web Workers概述

Web Workers是HTML5提供的API,允许JavaScript在后台线程中运行脚本,避免耗时操作阻塞主线程,从而提升Web应用的性能和用户体验。Web Workers独立于主线程运行,无法直接操作DOM,但可以通过消息机制与主线程通信。

东巴文(db-w.cn) 认为:Web Workers是Web应用多线程编程的基础,让JavaScript也能充分利用多核CPU的计算能力。

Web Workers特点

优点

特点 说明
后台执行 在独立线程中运行,不阻塞主线程
多核利用 充分利用多核CPU的计算能力
异步通信 通过消息机制与主线程通信
独立作用域 拥有独立的执行环境和内存空间

限制

限制 说明
不能操作DOM 无法访问document、window对象
不能访问全局变量 无法访问主线程的全局变量
同源限制 Worker脚本必须与主页面同源
文件限制 不能直接访问本地文件系统

Worker类型

专用Worker(Dedicated Worker)

专用Worker只能被创建它的脚本访问,一对一通信。

// 主线程
const worker = new Worker('worker.js');

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

// 接收消息
worker.onmessage = function(event) {
    console.log('收到Worker消息:', event.data);
};

// 错误处理
worker.onerror = function(error) {
    console.error('Worker错误:', error);
};
// worker.js
self.onmessage = function(event) {
    const { type, data } = event.data;
    
    if (type === 'start') {
        // 执行耗时计算
        const result = data.reduce((sum, num) => sum + num, 0);
        
        // 发送结果回主线程
        self.postMessage({ type: 'result', data: result });
    }
};

共享Worker(Shared Worker)

共享Worker可以被多个脚本访问,多对一通信。

// 主线程
const worker = new SharedWorker('shared-worker.js');

// 发送消息
worker.port.postMessage({ type: 'message', data: 'Hello' });

// 接收消息
worker.port.onmessage = function(event) {
    console.log('收到共享Worker消息:', event.data);
};

// 启动端口
worker.port.start();
// shared-worker.js
const ports = [];

self.onconnect = function(event) {
    const port = event.ports[0];
    ports.push(port);
    
    port.onmessage = function(event) {
        const { type, data } = event.data;
        
        // 广播消息给所有连接
        ports.forEach(p => {
            p.postMessage({ type: 'broadcast', data: data });
        });
    };
    
    port.start();
};

东巴文点评:专用Worker适合单一任务的并行处理,共享Worker适合多页面间的数据共享和通信。

创建Worker

使用外部文件

// 主线程
const worker = new Worker('scripts/worker.js');

worker.postMessage({ command: 'calculate', data: 100 });

worker.onmessage = function(event) {
    console.log('计算结果:', event.data);
};

worker.onerror = function(error) {
    console.error('Worker错误:', error.message);
};

使用Blob创建内联Worker

// 创建内联Worker
const workerCode = `
    self.onmessage = function(event) {
        const data = event.data;
        const result = data * 2;
        self.postMessage(result);
    };
`;

const blob = new Blob([workerCode], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
const worker = new Worker(workerUrl);

worker.postMessage(10);

worker.onmessage = function(event) {
    console.log('结果:', event.data); // 输出: 20
};

// 清理URL
URL.revokeObjectURL(workerUrl);

消息通信

发送消息

// 主线程发送消息
worker.postMessage({
    type: 'task',
    data: {
        id: 1,
        value: [1, 2, 3, 4, 5]
    }
});

// Worker发送消息
self.postMessage({
    type: 'result',
    data: result
});

接收消息

// 主线程接收消息
worker.onmessage = function(event) {
    const { type, data } = event.data;
    
    switch (type) {
        case 'result':
            console.log('结果:', data);
            break;
        case 'progress':
            console.log('进度:', data);
            break;
    }
};

// Worker接收消息
self.onmessage = function(event) {
    const { type, data } = event.data;
    // 处理消息
};

使用MessageChannel

const channel = new MessageChannel();

// 发送端口给Worker
worker.postMessage({ type: 'init' }, [channel.port2]);

// 主线程监听
channel.port1.onmessage = function(event) {
    console.log('主线程收到:', event.data);
};

// Worker中使用
self.onmessage = function(event) {
    if (event.data.type === 'init') {
        const port = event.ports[0];
        
        port.onmessage = function(e) {
            console.log('Worker收到:', e.data);
            port.postMessage('Worker回复');
        };
    }
};

东巴文点评:MessageChannel提供了更灵活的双向通信机制,适合复杂的通信场景。

Worker API

self对象

Worker中使用self对象表示Worker本身。

// worker.js
console.log(self); // DedicatedWorkerGlobalScope

// 监听消息
self.onmessage = function(event) {
    // 处理消息
};

// 发送消息
self.postMessage('Hello');

// 关闭Worker
self.close();

// 导入脚本
self.importScripts('utils.js', 'helper.js');

importScripts方法

// worker.js
// 导入单个脚本
importScripts('utils.js');

// 导入多个脚本
importScripts('utils.js', 'helper.js', 'constants.js');

// 使用导入的脚本
const result = calculateSum([1, 2, 3, 4, 5]);
self.postMessage(result);

错误处理

// 主线程
worker.onerror = function(error) {
    console.error('Worker错误:', error.message);
    console.error('文件:', error.filename);
    console.error('行号:', error.lineno);
    
    // 阻止错误继续传播
    error.preventDefault();
};

// Worker中
self.onerror = function(error) {
    console.error('Worker内部错误:', error);
    return true; // 阻止错误传播
};

// 使用try-catch
self.onmessage = function(event) {
    try {
        // 可能出错的代码
        const result = riskyOperation(event.data);
        self.postMessage({ success: true, data: result });
    } catch (error) {
        self.postMessage({ success: false, error: error.message });
    }
};

应用场景

1. 大数据计算

// 主线程
const worker = new Worker('calculate-worker.js');

worker.postMessage({
    type: 'fibonacci',
    n: 40
});

worker.onmessage = function(event) {
    console.log('斐波那契第40项:', event.data);
};

// calculate-worker.js
self.onmessage = function(event) {
    const { type, n } = event.data;
    
    if (type === 'fibonacci') {
        const result = fibonacci(n);
        self.postMessage(result);
    }
};

function fibonacci(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

2. 图像处理

// 主线程
const worker = new Worker('image-worker.js');

// 发送图像数据
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

worker.postMessage({
    type: 'grayscale',
    data: imageData
});

worker.onmessage = function(event) {
    const processedData = event.data;
    ctx.putImageData(processedData, 0, 0);
};

// image-worker.js
self.onmessage = function(event) {
    const { type, data } = event.data;
    
    if (type === 'grayscale') {
        const imageData = data;
        const pixels = imageData.data;
        
        for (let i = 0; i < pixels.length; i += 4) {
            const avg = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
            pixels[i] = avg;     // R
            pixels[i + 1] = avg; // G
            pixels[i + 2] = avg; // B
        }
        
        self.postMessage(imageData, [imageData.data.buffer]);
    }
};

东巴文点评:图像处理是Web Workers的经典应用场景,可以避免处理大量像素数据时阻塞UI。

3. 数据排序

// 主线程
const worker = new Worker('sort-worker.js');

const largeArray = Array.from({ length: 100000 }, () => Math.random());

worker.postMessage({
    type: 'sort',
    data: largeArray
});

worker.onmessage = function(event) {
    console.log('排序完成,前10个元素:', event.data.slice(0, 10));
};

// sort-worker.js
self.onmessage = function(event) {
    const { type, data } = event.data;
    
    if (type === 'sort') {
        const sorted = data.sort((a, b) => a - b);
        self.postMessage(sorted);
    }
};

4. 定时任务

// 主线程
const worker = new Worker('timer-worker.js');

worker.postMessage({
    type: 'start',
    interval: 1000
});

worker.onmessage = function(event) {
    console.log('定时器触发:', event.data);
};

// timer-worker.js
let timerId = null;

self.onmessage = function(event) {
    const { type, interval } = event.data;
    
    if (type === 'start') {
        timerId = setInterval(() => {
            self.postMessage(new Date().toISOString());
        }, interval);
    } else if (type === 'stop') {
        if (timerId) {
            clearInterval(timerId);
            timerId = null;
        }
    }
};

综合示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Web Workers示例 - 东巴文</title>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 800px;
            margin: 0 auto;
            padding: 20px;
        }
        
        .section {
            margin: 20px 0;
            padding: 20px;
            border: 1px solid #ddd;
            border-radius: 5px;
        }
        
        button {
            padding: 10px 20px;
            margin: 5px;
            border: none;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
        }
        
        button:hover {
            opacity: 0.9;
        }
        
        button:disabled {
            background: #ccc;
            cursor: not-allowed;
        }
        
        .result {
            margin: 10px 0;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 5px;
            font-family: monospace;
        }
        
        .progress {
            width: 100%;
            height: 20px;
            background: #e0e0e0;
            border-radius: 10px;
            overflow: hidden;
            margin: 10px 0;
        }
        
        .progress-bar {
            height: 100%;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            transition: width 0.3s;
        }
        
        canvas {
            border: 1px solid #ddd;
            margin: 10px 0;
        }
        
        input {
            padding: 8px;
            margin: 5px;
            border: 1px solid #ddd;
            border-radius: 3px;
        }
    </style>
</head>
<body>
    <h1>Web Workers示例</h1>
    
    <div class="section">
        <h2>斐波那契数列计算</h2>
        <p>计算斐波那契数列第n项(演示耗时计算)</p>
        <input type="number" id="fibInput" value="40" min="1" max="50">
        <button onclick="calculateFib()">计算</button>
        <button onclick="stopFib()">停止</button>
        <div class="result" id="fibResult">等待计算...</div>
    </div>
    
    <div class="section">
        <h2>大数据排序</h2>
        <p>在Worker中排序大量数据</p>
        <input type="number" id="sortInput" value="100000" min="1000" max="1000000">
        <button onclick="sortData()">排序</button>
        <div class="progress">
            <div class="progress-bar" id="sortProgress" style="width: 0%"></div>
        </div>
        <div class="result" id="sortResult">等待排序...</div>
    </div>
    
    <div class="section">
        <h2>图像处理</h2>
        <p>在Worker中处理图像(灰度化)</p>
        <canvas id="imageCanvas" width="300" height="200"></canvas>
        <br>
        <button onclick="processImage()">处理图像</button>
        <button onclick="resetImage()">重置</button>
        <div class="result" id="imageResult">等待处理...</div>
    </div>
    
    <div class="section">
        <h2>主线程响应测试</h2>
        <p>测试Worker运行时主线程是否响应</p>
        <button onclick="testMainThread()">点击测试</button>
        <div class="result" id="mainThreadResult">点击次数: 0</div>
    </div>
    
    <script>
        // Worker管理器
        class WorkerManager {
            constructor() {
                this.workers = {};
            }
            
            create(name, workerCode) {
                if (this.workers[name]) {
                    this.workers[name].terminate();
                }
                
                const blob = new Blob([workerCode], { type: 'application/javascript' });
                const url = URL.createObjectURL(blob);
                this.workers[name] = new Worker(url);
                URL.revokeObjectURL(url);
                
                return this.workers[name];
            }
            
            get(name) {
                return this.workers[name];
            }
            
            terminate(name) {
                if (this.workers[name]) {
                    this.workers[name].terminate();
                    delete this.workers[name];
                }
            }
            
            terminateAll() {
                Object.values(this.workers).forEach(worker => worker.terminate());
                this.workers = {};
            }
        }
        
        const workerManager = new WorkerManager();
        
        // 斐波那契Worker代码
        const fibWorkerCode = `
            self.onmessage = function(event) {
                const { type, n } = event.data;
                
                if (type === 'calculate') {
                    const result = fibonacci(n);
                    self.postMessage({ type: 'result', data: result });
                }
            };
            
            function fibonacci(n) {
                if (n <= 1) return n;
                return fibonacci(n - 1) + fibonacci(n - 2);
            }
        `;
        
        // 排序Worker代码
        const sortWorkerCode = `
            self.onmessage = function(event) {
                const { type, data } = event.data;
                
                if (type === 'sort') {
                    // 发送进度
                    self.postMessage({ type: 'progress', data: 0 });
                    
                    // 模拟排序过程
                    const sorted = [...data].sort((a, b) => a - b);
                    
                    self.postMessage({ type: 'progress', data: 50 });
                    
                    // 返回结果
                    self.postMessage({ 
                        type: 'result', 
                        data: {
                            sorted: sorted.slice(0, 10),
                            length: sorted.length
                        }
                    });
                    
                    self.postMessage({ type: 'progress', data: 100 });
                }
            };
        `;
        
        // 图像处理Worker代码
        const imageWorkerCode = `
            self.onmessage = function(event) {
                const { type, data } = event.data;
                
                if (type === 'grayscale') {
                    const pixels = data.data;
                    
                    for (let i = 0; i < pixels.length; i += 4) {
                        const avg = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
                        pixels[i] = avg;     // R
                        pixels[i + 1] = avg; // G
                        pixels[i + 2] = avg; // B
                    }
                    
                    self.postMessage({ type: 'result', data: data }, [data.data.buffer]);
                }
            };
        `;
        
        // 斐波那契计算
        function calculateFib() {
            const n = parseInt(document.getElementById('fibInput').value);
            const resultEl = document.getElementById('fibResult');
            
            resultEl.textContent = '计算中...';
            
            const worker = workerManager.create('fib', fibWorkerCode);
            
            worker.onmessage = function(event) {
                const { type, data } = event.data;
                
                if (type === 'result') {
                    resultEl.textContent = `斐波那契第${n}项: ${data}`;
                }
            };
            
            worker.onerror = function(error) {
                resultEl.textContent = '错误: ' + error.message;
            };
            
            worker.postMessage({ type: 'calculate', n: n });
        }
        
        function stopFib() {
            workerManager.terminate('fib');
            document.getElementById('fibResult').textContent = '已停止';
        }
        
        // 数据排序
        function sortData() {
            const length = parseInt(document.getElementById('sortInput').value);
            const resultEl = document.getElementById('sortResult');
            const progressEl = document.getElementById('sortProgress');
            
            resultEl.textContent = '生成数据中...';
            progressEl.style.width = '0%';
            
            // 生成随机数据
            const data = Array.from({ length: length }, () => Math.random());
            
            resultEl.textContent = '排序中...';
            
            const worker = workerManager.create('sort', sortWorkerCode);
            
            worker.onmessage = function(event) {
                const { type, data } = event.data;
                
                if (type === 'progress') {
                    progressEl.style.width = data + '%';
                } else if (type === 'result') {
                    resultEl.textContent = `排序完成!数据量: ${data.length}, 前10个: [${data.sorted.map(n => n.toFixed(4)).join(', ')}]`;
                }
            };
            
            worker.postMessage({ type: 'sort', data: data });
        }
        
        // 图像处理
        function processImage() {
            const canvas = document.getElementById('imageCanvas');
            const ctx = canvas.getContext('2d');
            const resultEl = document.getElementById('imageResult');
            
            // 绘制初始图像
            const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
            gradient.addColorStop(0, '#ff0000');
            gradient.addColorStop(0.5, '#00ff00');
            gradient.addColorStop(1, '#0000ff');
            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            resultEl.textContent = '处理中...';
            
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
            
            const worker = workerManager.create('image', imageWorkerCode);
            
            worker.onmessage = function(event) {
                const { type, data } = event.data;
                
                if (type === 'result') {
                    ctx.putImageData(data, 0, 0);
                    resultEl.textContent = '处理完成!';
                }
            };
            
            worker.postMessage({ type: 'grayscale', data: imageData }, [imageData.data.buffer]);
        }
        
        function resetImage() {
            const canvas = document.getElementById('imageCanvas');
            const ctx = canvas.getContext('2d');
            const resultEl = document.getElementById('imageResult');
            
            const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
            gradient.addColorStop(0, '#ff0000');
            gradient.addColorStop(0.5, '#00ff00');
            gradient.addColorStop(1, '#0000ff');
            ctx.fillStyle = gradient;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            
            resultEl.textContent = '已重置';
        }
        
        // 主线程响应测试
        let clickCount = 0;
        
        function testMainThread() {
            clickCount++;
            document.getElementById('mainThreadResult').textContent = `点击次数: ${clickCount}`;
        }
        
        // 初始化图像
        resetImage();
        
        // 页面卸载时清理Worker
        window.addEventListener('beforeunload', function() {
            workerManager.terminateAll();
        });
    </script>
</body>
</html>

最佳实践

1. 合理使用Worker

// 推荐:耗时任务使用Worker
function processLargeData(data) {
    const worker = new Worker('data-processor.js');
    worker.postMessage(data);
    return new Promise((resolve, reject) => {
        worker.onmessage = (e) => resolve(e.data);
        worker.onerror = (e) => reject(e.error);
    });
}

// 不推荐:简单任务使用Worker(通信开销大于计算开销)
function add(a, b) {
    // 简单加法不需要Worker
    return a + b;
}

2. 错误处理

// 推荐:完善的错误处理
const worker = new Worker('worker.js');

worker.onerror = function(error) {
    console.error('Worker错误:', {
        message: error.message,
        filename: error.filename,
        lineno: error.lineno,
        colno: error.colno
    });
    
    // 阻止错误传播
    error.preventDefault();
};

worker.onmessageerror = function(error) {
    console.error('消息序列化错误:', error);
};

// 不推荐:忽略错误处理
const worker = new Worker('worker.js');
worker.postMessage(data);

3. 资源清理

// 推荐:及时清理Worker
function processWithWorker(data) {
    const worker = new Worker('worker.js');
    
    worker.onmessage = function(event) {
        // 处理完成后立即终止
        worker.terminate();
        console.log('结果:', event.data);
    };
    
    worker.onerror = function(error) {
        // 出错时也要终止
        worker.terminate();
        console.error('错误:', error);
    };
    
    worker.postMessage(data);
}

// 不推荐:不清理Worker
const worker = new Worker('worker.js');
// 使用后忘记terminate,导致内存泄漏

东巴文点评:Worker是宝贵的资源,使用完毕后应该及时终止,避免内存泄漏。

4. 数据传输优化

// 推荐:使用Transferable对象
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

// 传输所有权,避免复制
worker.postMessage(imageData, [imageData.data.buffer]);

// 不推荐:传输大对象时复制数据
worker.postMessage(imageData); // 会复制整个imageData

学习检验

知识点测试

问题1:Web Workers中不能直接操作以下哪个对象?

A. self B. navigator C. document D. location

<details> <summary>点击查看答案</summary>

答案:C

东巴文解释:Web Workers运行在独立线程中,无法直接访问DOM,因此不能操作document对象。但可以访问selfnavigatorlocation等对象。

</details>

问题2:以下哪个方法用于在Worker中导入外部脚本?

A. import() B. importScripts() C. loadScript() D. require()

<details> <summary>点击查看答案</summary>

答案:B

东巴文解释:在Worker中使用importScripts()方法导入外部脚本,可以导入一个或多个脚本文件。

</details>

实践任务

任务:创建一个Web Worker,实现大数组的过滤功能,过滤出所有大于指定值的元素。

<details> <summary>点击查看参考答案</summary>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>数组过滤Worker</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            max-width: 600px;
            margin: 50px auto;
            padding: 20px;
        }
        
        input {
            padding: 8px;
            margin: 10px 5px;
            border: 1px solid #ddd;
            border-radius: 3px;
        }
        
        button {
            padding: 10px 20px;
            border: none;
            background: #667eea;
            color: white;
            border-radius: 5px;
            cursor: pointer;
        }
        
        .result {
            margin: 20px 0;
            padding: 15px;
            background: #f5f5f5;
            border-radius: 5px;
        }
    </style>
</head>
<body>
    <h1>数组过滤Worker</h1>
    
    <div>
        <label>数组大小:</label>
        <input type="number" id="arraySize" value="100000">
    </div>
    
    <div>
        <label>过滤阈值:</label>
        <input type="number" id="threshold" value="0.5" step="0.1">
    </div>
    
    <button onclick="filterArray()">开始过滤</button>
    
    <div class="result" id="result">等待过滤...</div>
    
    <script>
        // Worker代码
        const workerCode = `
            self.onmessage = function(event) {
                const { type, data, threshold } = event.data;
                
                if (type === 'filter') {
                    const filtered = data.filter(item => item > threshold);
                    
                    self.postMessage({
                        type: 'result',
                        data: {
                            originalLength: data.length,
                            filteredLength: filtered.length,
                            filtered: filtered.slice(0, 10) // 只返回前10个
                        }
                    });
                }
            };
        `;
        
        // 创建Worker
        const blob = new Blob([workerCode], { type: 'application/javascript' });
        const workerUrl = URL.createObjectURL(blob);
        const worker = new Worker(workerUrl);
        
        worker.onmessage = function(event) {
            const { type, data } = event.data;
            
            if (type === 'result') {
                document.getElementById('result').innerHTML = `
                    <p>原始数组大小: ${data.originalLength}</p>
                    <p>过滤后大小: ${data.filteredLength}</p>
                    <p>前10个元素: [${data.filtered.map(n => n.toFixed(4)).join(', ')}]</p>
                `;
            }
        };
        
        function filterArray() {
            const size = parseInt(document.getElementById('arraySize').value);
            const threshold = parseFloat(document.getElementById('threshold').value);
            
            // 生成随机数组
            const array = Array.from({ length: size }, () => Math.random());
            
            document.getElementById('result').textContent = '过滤中...';
            
            // 发送给Worker
            worker.postMessage({
                type: 'filter',
                data: array,
                threshold: threshold
            });
        }
        
        // 清理
        window.addEventListener('beforeunload', function() {
            worker.terminate();
            URL.revokeObjectURL(workerUrl);
        });
    </script>
</body>
</html>
</details>

东巴文(db-w.cn) - 让编程学习更有趣、更高效!