前端WebWorker笔记总结

Web Worker 为 JavaScript 提供了一种在主线程之外的后台线程中执行脚本的能力。这对于处理计算密集型任务或避免长时间运行的脚本阻塞用户界面至关重要,从而提高应用的响应性和用户体验。

第一部分:Web Worker 概述

  1. 什么是 Web Worker?

    Web Worker 是浏览器提供的一种技术,允许脚本在独立于主执行线程的后台线程中运行。主线程通常负责处理用户界面(UI)的更新和响应用户交互,如果在这个线程中执行耗时过长的 JavaScript 代码,会导致页面卡顿甚至无响应。Web Worker 通过将这些任务转移到单独的线程,使得主线程能够保持流畅。 [1][2]

  2. 为什么使用 Web Worker?

    • 处理耗时计算 :对于需要大量计算的任务(如复杂算法、大规模数据处理、图像/视频处理等),Web Worker 可以防止这些任务阻塞主线程,从而避免页面冻结。 [3][4]
    • 提升用户体验 :通过确保主线程的流畅性,用户可以持续与页面交互,即使后台正在进行复杂操作。 [3]
    • 并行处理 :Web Worker 使得 JavaScript 能够利用多核处理器的优势,实现一定程度的并行计算。 [5][6]
  3. Web Worker 的类型

    Web Worker 主要有以下几种类型:

    • Dedicated Workers (专用 Worker) :由创建它的脚本(主线程)专用的 Worker。它们与父页面一对一通信。这是最常用的 Worker 类型,也是本文重点讲解的内容。 [2]
    • Shared Workers (共享 Worker) :可以被多个不同的窗口、iframe 或其他 Worker(只要它们同源)共享的 Worker。 [2][7]
    • Service Workers (服务 Worker) :一种特殊的 Worker,主要用于实现离线缓存、推送通知、后台同步等功能。它充当了 Web 应用、浏览器和网络之间的代理服务器。其生命周期和用途与计算型的 Dedicated Worker 和 Shared Worker 有显著区别。 [8]

第二部分:Dedicated Worker 详解

  1. 创建 Worker

    在主线程中,通过 Worker 构造函数创建一个新的 Dedicated Worker。构造函数的参数是 Worker 脚本文件的 URL。 [1][9]

    js 复制代码
    // main.js (主线程脚本)
    if (window.Worker) { // 检查浏览器是否支持 Web Worker
        const myWorker = new Worker('worker.js'); // 'worker.js' 是 Worker 脚本的路径
        console.log('Worker 已创建');
    } else {
        console.log('你的浏览器不支持 Web Workers.');
    }
    • 同源策略 :Worker 脚本文件必须遵守同源策略,即它必须与主页面同源(协议、域名、端口相同)。 [1][4] 如果脚本加载失败(如 404 错误),Worker 会静默失败。 [1]
  2. Worker 脚本 (worker.js)

    Worker 脚本在一个与主线程隔离的全局上下文中运行。在这个上下文中,self 关键字指向 Worker 的全局作用域 (DedicatedWorkerGlobalScope),类似于主线程中的 window[2][10]

    js 复制代码
    // worker.js (Worker 线程脚本)
    console.log('Worker 脚本开始执行');
    
    // self 关键字代表 Worker 的全局作用域
    self.onmessage = function(event) {
        console.log('Worker 接收到消息:', event.data);
        const result = event.data.num1 + event.data.num2;
        self.postMessage('结果: ' + result); // 将结果发送回主线程
    };
    
    // 也可以使用 addEventListener
    // self.addEventListener('message', function(event) {
    //     console.log('Worker 接收到消息 (通过 addEventListener):', event.data);
    // });
    
    console.log('Worker 等待消息...');
  3. 主线程与 Worker 线程通信

    主线程和 Worker 线程之间通过消息传递进行通信。双方都使用 postMessage() 方法发送消息,并通过 onmessage 事件处理函数或 addEventListener('message', ...) 来接收消息。 [1][2]

    • 主线程发送消息给 Worker

      js 复制代码
      // main.js
      const myWorker = new Worker('worker.js');
      myWorker.postMessage({ message: '你好 Worker,我是主线程', data: [1, 2, 3] });
      myWorker.postMessage('这是一条字符串消息');

      postMessage() 方法的参数可以是各种数据类型,包括复杂的 JavaScript 对象和二进制数据。 [1][11]

    • Worker 接收消息
      onmessage 处理函数的事件对象 event 有一个 data 属性,包含了从主线程发送过来的数据。 [1]

      js 复制代码
      // worker.js
      self.onmessage = function(event) {
          console.log('Worker 从主线程接收到数据:', event.data);
          if (typeof event.data === 'object' && event.data.message) {
              console.log('消息内容:', event.data.message);
              console.log('附带数据:', event.data.data);
          } else {
              console.log('接收到的原始数据:', event.data);
          }
      };
    • Worker 发送消息给主线程

      js 复制代码
      // worker.js
      self.onmessage = function(event) {
          const receivedData = event.data;
          // 进行一些处理...
          const processedData = receivedData.toUpperCase(); // 假设是字符串
          self.postMessage(processedData); // 将处理后的数据发送回主线程
      };
    • 主线程接收消息

      js 复制代码
      // main.js
      myWorker.onmessage = function(event) {
          console.log('主线程从 Worker 接收到数据:', event.data);
          // 更新 UI 或进行其他操作
      };
  4. 数据传递:结构化克隆算法 (Structured Clone Algorithm)

    当通过 postMessage() 传递数据时,数据并不是通过引用共享的,而是通过结构化克隆算法进行复制。这意味着 Worker 线程和主线程拥有各自独立的数据副本,修改一方的数据不会影响另一方。 [1][12]

    • 可以传递的数据类型

      • 基本类型 (null, undefined, boolean, number, string, bigint, symbol (如果 symbol 是全局注册的))
      • Boolean, String, Date, RegExp 对象
      • Array, Object (普通对象,包括循环引用) [13]
      • Map, Set
      • ArrayBuffer, TypedArray (如 Uint8Array)
      • Blob, File, FileList
      • ImageData
      • ImageBitmap, OffscreenCanvas (这些通常用于图形处理)
      • 等等。
    • 不能传递的数据类型

      • 函数 (Function 对象):会导致错误。 [14]
      • Error 对象:会导致错误。
      • DOM 节点:Worker 无法直接访问 DOM。 [1]
      • 某些内置对象实例,如 WeakMap, WeakSet,以及一些特定于浏览器的对象。
    • 注意 :虽然 undefined 可以被结构化克隆,但在对象中作为属性值时,如果使用 JSON.stringify 可能会被忽略,而结构化克隆会保留它。 [14]

  5. Transferable Objects (可转移对象)

    对于某些类型的对象,特别是 ArrayBufferMessagePortImageBitmap,可以使用一种称为"可转移对象"的机制来传递数据。这种方式不是复制数据,而是将对象的所有权从一个上下文(如主线程)转移到另一个上下文(如 Worker)。 [1][15]

    • 优点 :零拷贝,性能极高,尤其适用于大数据(如大型二进制文件、图像数据)。 [15][16]

    • 用法postMessage() 方法的第二个参数是一个数组,列出要转移的对象。

      js 复制代码
      // main.js
      const largeBuffer = new ArrayBuffer(1024 * 1024 * 32); // 32MB
      // 填充 buffer...
      const uint8Array = new Uint8Array(largeBuffer);
      for (let i = 0; i < uint8Array.length; i++) {
          uint8Array[i] = i % 256;
      }
      
      console.log('主线程: 发送前 ArrayBuffer 长度:', largeBuffer.byteLength); // 33554432
      myWorker.postMessage(largeBuffer, [largeBuffer]); // 转移 largeBuffer
      console.log('主线程: 发送后 ArrayBuffer 长度:', largeBuffer.byteLength); // 0,因为所有权已转移
      // 此时,主线程中的 largeBuffer 不能再被访问,尝试访问会出错或得到无效数据。
    • Worker 接收

      js 复制代码
      // worker.js
      self.onmessage = function(event) {
          const receivedBuffer = event.data; // 这是转移过来的 ArrayBuffer
          console.log('Worker: 接收到的 ArrayBuffer 长度:', receivedBuffer.byteLength);
          const receivedView = new Uint8Array(receivedBuffer);
          // 可以操作 receivedBuffer/receivedView
          // ...
          // 如果需要,可以将它再转移回主线程
          // self.postMessage(receivedBuffer, [receivedBuffer]);
      };
    • 支持的对象ArrayBuffer, MessagePort, ReadableStream, WritableStream, TransformStream, ImageBitmap, OffscreenCanvas, RTCDataChannel, AudioData, VideoFrame[17][18]

  6. 错误处理

    • 主线程监听 Worker 错误 :如果 Worker 内部发生未被捕获的错误,主线程的 Worker 对象会触发 error 事件。 [1][2]

      javascript 复制代码
      // main.js
      myWorker.onerror = function(errorEvent) {
          console.error('主线程捕获到 Worker 错误:');
          console.error('  消息:', errorEvent.message);
          console.error('  文件名:', errorEvent.filename);
          console.error('  行号:', errorEvent.lineno);
          errorEvent.preventDefault(); // 可以阻止默认的错误处理(如在控制台打印)
      };
    • Worker 内部捕获错误 :可以在 Worker 脚本中使用 try...catch 语句来捕获和处理错误。

      js 复制代码
      // worker.js
      self.onmessage = function(event) {
          try {
              // 可能会出错的代码
              if (event.data.action === 'doSomethingRisky') {
                  throw new Error('Worker 内部故意抛出的错误');
              }
              // ...
          } catch (e) {
              console.error('Worker 内部捕获到错误:', e.message);
              // 可以选择将错误信息发送回主线程
              self.postMessage({ type: 'error', message: e.message, stack: e.stack });
          }
      };
    • Worker 内部主动报告错误 :如上例所示,Worker 可以通过 postMessage 发送自定义的错误对象给主线程。

    • onmessageerror 事件 :如果在 postMessage 序列化数据时发生错误(例如,尝试传递一个不可序列化的对象),发送方的 onmessageerror 事件会被触发(如果监听了的话),或者接收方的 onmessageerror(对于 Worker 内部的 self)。 [13]

  7. 终止 Worker

    当不再需要 Worker 时,应该终止它以释放资源。 [1]

    • 主线程终止 Worker

      js 复制代码
      // main.js
      myWorker.terminate();
      console.log('Worker 已被主线程终止');

      terminate() 会立即终止 Worker 线程的执行,Worker 将没有机会完成当前操作或进行清理。 [19]

    • Worker 自我终止

      js 复制代码
      // worker.js
      // ... 完成任务后 ...
      console.log('Worker 完成任务,自行关闭');
      self.close();

      self.close() 允许 Worker 在完成其工作后自行关闭。 [19]

  8. 在 Worker 中引入外部脚本

    Worker 可以使用 importScripts() 方法同步加载一个或多个外部 JavaScript 文件到其作用域中。 [8][20]

    js 复制代码
    // worker.js
    console.log('Worker 脚本执行');
    try {
        importScripts('helperUtil.js', 'anotherScript.js'); // 同步加载脚本
        // 现在 helperUtil.js 和 anotherScript.js 中的函数和变量可以在这里使用
        const utilityResult = utilityFunctionFromHelper(10);
        console.log('从 helperUtil.js 调用的结果:', utilityResult);
        anotherFunction();
    } catch (e) {
        console.error('Worker 导入脚本失败:', e);
        self.postMessage({ type: 'error', message: '导入脚本失败: ' + e.message });
    }
    
    self.onmessage = function(event) {
        // ...
    };
    • importScripts() 的参数是脚本的 URL,可以是相对路径或绝对路径(必须同源)。

    • 脚本是同步加载和执行的,如果某个脚本加载失败,会抛出错误,后续的脚本也不会被执行。 [21]

    • 也可以在创建 Worker 时指定 type: 'module',然后在 Worker 脚本内部使用 ES6 的 import 语法来异步加载模块。 [19][20]

      js 复制代码
      // main.js
      const moduleWorker = new Worker('module_worker.js', { type: 'module' });
      
      // module_worker.js
      // import { someFunction } from './module_helper.js';
      // console.log(someFunction());
      // self.onmessage = ...
  9. Worker 的限制

    由于 Worker 在独立的线程中运行,它们有一些重要的限制: [1][2]

    • 无 DOM 访问 :Worker 不能直接访问或操作主页面的 DOM 结构(如 document 对象)。 [1][22] 这是为了防止线程安全问题。

    • 有限的 window 对象访问 :Worker 不能直接访问主线程的 window 对象。self 在 Worker 中指向其自身的全局作用域 (DedicatedWorkerGlobalScopeSharedWorkerGlobalScope)。 [2][10]

    • 可访问的 API:尽管有上述限制,Worker 仍然可以访问许多 Web API,例如:

      • self:Worker 的全局作用域。
      • navigator:提供浏览器和系统信息。
      • location:只读的,包含 Worker 脚本的 URL 信息。
      • XMLHttpRequestfetch:用于发送网络请求。 [2]
      • setTimeout(), setInterval(), clearTimeout(), clearInterval()
      • console 对象。
      • postMessage(), onmessage, onerror
      • importScripts() (对于非模块 Worker)。
      • close()
      • WebSockets, IndexedDB, Cache API, Promise, Crypto API 等。
    • 无法执行的方法 :不能使用 alert(), confirm(), prompt() 等会阻塞用户界面的方法。 [1]

第三部分:详细代码示例与讲解

为了更好地理解 Web Worker,我们将通过几个示例来演示其用法。每个示例通常包含一个 HTML 文件、一个主 JavaScript 文件和一个 Worker JavaScript 文件。

示例1:基础的 Dedicated Worker - 简单消息传递

这个例子展示了主线程和 Worker 之间的基本双向通信。

  • index_basic.html:

    html 复制代码
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Web Worker 基础示例</title>
        <style>
            body { font-family: sans-serif; padding: 20px; }
            #output { margin-top: 20px; border: 1px solid #ccc; padding: 10px; min-height: 50px; }
            button { padding: 10px 15px; margin-right: 10px; }
        </style>
    </head>
    <body>
        <h1>Web Worker 基础示例</h1>
        <input type="text" id="messageInput" placeholder="输入要发送给 Worker 的消息">
        <button id="sendMessageButton">发送消息给 Worker</button>
        <button id="terminateWorkerButton">终止 Worker</button>
        <div id="output">等待 Worker 的响应...</div>
    
        <script src="main_basic.js"></script>
    </body>
    </html>
  • main_basic.js (主线程逻辑):

    js 复制代码
    // main_basic.js
    console.log("主线程脚本 (main_basic.js) 开始执行");
    
    const messageInput = document.getElementById('messageInput');
    const sendMessageButton = document.getElementById('sendMessageButton');
    const terminateWorkerButton = document.getElementById('terminateWorkerButton');
    const outputDiv = document.getElementById('output');
    
    let myWorker; // 将 Worker 实例保存在变量中,以便后续操作
    
    if (window.Worker) {
        console.log("浏览器支持 Web Worker。正在创建 Worker...");
        try {
            myWorker = new Worker('basic_worker.js');
            console.log("Worker (basic_worker.js) 已创建。");
    
            // --- 1. 主线程向 Worker 发送消息 ---
            sendMessageButton.addEventListener('click', () => {
                const messageText = messageInput.value.trim();
                if (messageText) {
                    console.log(`主线程: 准备发送消息 "${messageText}" 给 Worker。`);
                    myWorker.postMessage(messageText);
                    outputDiv.innerHTML = `主线程: 已发送消息 "${messageText}" 给 Worker。等待响应...`;
                    messageInput.value = ''; // 清空输入框
                } else {
                    outputDiv.innerHTML = '主线程: 请输入要发送的消息。';
                }
            });
    
            // --- 2. 主线程接收来自 Worker 的消息 ---
            myWorker.onmessage = function(event) {
                const workerResponse = event.data;
                console.log("主线程: 从 Worker 接收到消息:", workerResponse);
                outputDiv.innerHTML = `主线程: Worker 回复: "${workerResponse}"`;
            };
    
            // --- 3. 主线程处理 Worker 发生的错误 ---
            myWorker.onerror = function(errorEvent) {
                console.error("主线程: 捕获到 Worker 错误。");
                console.error("  错误消息:", errorEvent.message);
                console.error("  所在文件:", errorEvent.filename);
                console.error("  所在行号:", errorEvent.lineno);
                outputDiv.innerHTML = `主线程: Worker 发生错误: ${errorEvent.message}`;
                // 阻止事件的默认行为 (例如,在控制台打印未捕获的错误)
                errorEvent.preventDefault();
            };
    
            // --- 4. 主线程终止 Worker ---
            terminateWorkerButton.addEventListener('click', () => {
                if (myWorker) {
                    myWorker.terminate();
                    console.log("主线程: Worker 已被终止。");
                    outputDiv.innerHTML = '主线程: Worker 已被终止。它将不再响应。';
                    sendMessageButton.disabled = true; // 禁用发送按钮
                    terminateWorkerButton.disabled = true; // 禁用终止按钮
                }
            });
    
        } catch (e) {
            console.error("主线程: 创建 Worker 时发生错误:", e);
            outputDiv.innerHTML = `主线程: 创建 Worker 失败: ${e.message}`;
        }
    } else {
        console.warn("你的浏览器不支持 Web Workers。");
        outputDiv.innerHTML = '抱歉,你的浏览器不支持 Web Workers。此示例无法运行。';
        sendMessageButton.disabled = true;
        terminateWorkerButton.disabled = true;
    }
    
    console.log("主线程脚本 (main_basic.js) 初始化完毕。");
  • basic_worker.js (Worker 逻辑):

    js 复制代码
    // basic_worker.js
    console.log("Worker 线程 (basic_worker.js) 开始执行。");
    
    // --- 1. Worker 接收来自主线程的消息 ---
    self.onmessage = function(event) {
        const messageFromMain = event.data;
        console.log(`Worker: 从主线程接收到消息: "${messageFromMain}"`);
    
        // 模拟一些处理
        const processedMessage = `你好主线程,我收到了你的消息: "${messageFromMain}". 我的处理结果是: ${messageFromMain.toUpperCase()}`;
        console.log(`Worker: 正在将处理后的消息 "${processedMessage}" 发送回主线程。`);
    
        // --- 2. Worker 向主线程发送消息 ---
        self.postMessage(processedMessage);
    };
    
    // Worker 也可以监听错误事件 (通常用于 Worker 内部的未捕获错误,但这里的示例主要演示主线程的 onerror)
    // self.onerror = function(error) {
    //     console.error('Worker 内部发生错误:', error.message);
    //     // 可以尝试将错误信息发送给主线程,但这可能在某些情况下不可靠
    //     // self.postMessage({ type: 'WORKER_ERROR', message: error.message });
    // };
    
    // 模拟一个可能导致 Worker 内部错误的函数 (如果主线程发送 'CRASH' 消息)
    function potentiallyCrashingFunction() {
        console.log("Worker: 正在执行可能崩溃的函数...");
        // 这会抛出一个未捕获的错误,应该会被主线程的 worker.onerror 捕获
        let obj = null;
        return obj.property; // TypeError: Cannot read properties of null
    }
    
    // 修改 onmessage 以包含错误触发逻辑
    self.onmessage = function(event) {
        const messageFromMain = event.data;
        console.log(`Worker: 从主线程接收到消息: "${messageFromMain}"`);
    
        if (messageFromMain === 'CRASH') {
            console.warn("Worker: 收到 'CRASH' 命令,将尝试执行导致错误的操作。");
            try {
                potentiallyCrashingFunction();
            } catch (e) {
                // 如果 Worker 内部捕获了错误,主线程的 onerror 不会触发
                console.error("Worker: 内部捕获到错误:", e.message);
                self.postMessage(`Worker 内部捕获到错误: ${e.message}`);
            }
            // 如果不捕获,错误会传播到主线程的 onerror
            // potentiallyCrashingFunction(); // 取消注释这行,并注释掉 try-catch 块来测试主线程的 onerror
            return; // 避免后续的 postMessage
        }
    
        const processedMessage = `你好主线程,我收到了你的消息: "${messageFromMain}". 我的处理结果是: ${messageFromMain.toUpperCase()}`;
        console.log(`Worker: 正在将处理后的消息 "${processedMessage}" 发送回主线程。`);
        self.postMessage(processedMessage);
    };
    
    
    console.log("Worker 线程 (basic_worker.js) 已准备好接收消息。");

    代码讲解 (示例1):

    • index_basic.html:

      • 包含一个输入框 (messageInput) 让用户输入消息,一个发送按钮 (sendMessageButton),一个终止按钮 (terminateWorkerButton),以及一个 div (output) 用于显示主线程和 Worker 的交互信息。
      • 引入 main_basic.js 脚本。
    • main_basic.js:

      • 首先检查 window.Worker 是否存在,以判断浏览器是否支持 Web Worker。
      • myWorker = new Worker('basic_worker.js'); 创建一个新的 Worker 实例,加载 basic_worker.js 文件。
      • sendMessageButton.addEventListener('click', ...): 当用户点击发送按钮时,获取输入框的内容,并通过 myWorker.postMessage(messageText) 将其发送给 Worker。
      • myWorker.onmessage = function(event) { ... }: 设置一个事件监听器,当 Worker 通过 self.postMessage() 发送消息回主线程时,此函数会被调用。event.data 包含 Worker 发送的数据。
      • myWorker.onerror = function(errorEvent) { ... }: 设置一个错误监听器。如果 Worker 脚本在执行过程中发生未被捕获的错误,此函数会被调用。errorEvent 对象包含错误信息(如消息、文件名、行号)。
      • terminateWorkerButton.addEventListener('click', ...): 点击终止按钮时,调用 myWorker.terminate() 来立即停止 Worker 的执行。
    • basic_worker.js:

      • self.onmessage = function(event) { ... }: Worker 脚本的核心。当主线程调用 worker.postMessage() 时,此函数在 Worker 线程中被触发。event.data 包含从主线程发送过来的数据。
      • Worker 接收到数据后,进行一些处理(这里是转换为大写并添加一些文本),然后通过 self.postMessage(processedMessage) 将结果发送回主线程。
      • 示例中还包含了一个 potentiallyCrashingFunction 和一个检查 'CRASH' 消息的逻辑,用于演示错误处理。如果 Worker 内部没有 try...catch 块来捕获错误,该错误会冒泡到主线程的 worker.onerror 处理函数。

示例2:处理 CPU 密集型任务 - 计算斐波那契数列

这个例子演示了如何使用 Web Worker 来执行一个耗时的计算(计算斐波那契数列的第 N 项),而不会阻塞主线程的 UI。

  • index_fib.html:

    html 复制代码
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Web Worker - CPU 密集型任务</title>
        <style>
            body { font-family: sans-serif; padding: 20px; }
            #controls, #resultArea, #statusArea { margin-bottom: 15px; }
            input[type="number"] { width: 80px; padding: 8px; }
            button { padding: 8px 12px; margin-left: 10px; }
            #result, #status { font-weight: bold; }
            .loader {
                border: 4px solid #f3f3f3; border-radius: 50%; border-top: 4px solid #3498db;
                width: 20px; height: 20px; animation: spin 1s linear infinite; display: none;
            }
            @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
        </style>
    </head>
    <body>
        <h1>计算斐波那契数列 (使用 Web Worker)</h1>
        <div id="controls">
            <label for="fibInput">计算斐波那契数列的第 N 项 (N):</label>
            <input type="number" id="fibInput" value="35" min="1" max="45">
            <button id="calculateButton">开始计算 (使用 Worker)</button>
            <button id="calculateButtonMainThread">开始计算 (在主线程 - 会卡顿)</button>
        </div>
        <div id="statusArea">
            状态: <span id="status">空闲</span>
            <div class="loader" id="loader"></div>
        </div>
        <div id="resultArea">
            结果: <span id="result">尚未计算</span>
        </div>
        <hr>
        <p>尝试在计算期间与页面交互 (例如,点击下面的按钮或输入框)。</p>
        <input type="text" placeholder="测试输入框是否卡顿">
        <button onclick="alert('UI 响应测试!')">测试 UI 响应</button>
    
        <script src="main_fib.js"></script>
    </body>
    </html>
  • main_fib.js (主线程逻辑):

    js 复制代码
    // main_fib.js
    console.log("主线程脚本 (main_fib.js) 开始执行");
    
    const fibInput = document.getElementById('fibInput');
    const calculateButton = document.getElementById('calculateButton');
    const calculateButtonMainThread = document.getElementById('calculateButtonMainThread');
    const resultSpan = document.getElementById('result');
    const statusSpan = document.getElementById('status');
    const loaderDiv = document.getElementById('loader');
    
    let fibWorker;
    
    if (window.Worker) {
        console.log("创建斐波那契 Worker...");
        fibWorker = new Worker('fib_worker.js');
    
        calculateButton.addEventListener('click', () => {
            const n = parseInt(fibInput.value);
            if (isNaN(n) || n <= 0) {
                resultSpan.textContent = "请输入一个有效的正整数。";
                return;
            }
            console.log(`主线程: 请求 Worker 计算斐波那契数 F(${n})`);
            resultSpan.textContent = "计算中...";
            statusSpan.textContent = `正在计算 F(${n}) (使用 Worker)...`;
            loaderDiv.style.display = 'inline-block';
            calculateButton.disabled = true;
            calculateButtonMainThread.disabled = true;
    
            fibWorker.postMessage(n);
        });
    
        fibWorker.onmessage = function(event) {
            const fibResult = event.data;
            console.log(`主线程: Worker 返回斐波那契结果: ${fibResult}`);
            resultSpan.textContent = String(fibResult);
            statusSpan.textContent = "计算完成 (使用 Worker)。";
            loaderDiv.style.display = 'none';
            calculateButton.disabled = false;
            calculateButtonMainThread.disabled = false;
        };
    
        fibWorker.onerror = function(error) {
            console.error("主线程: Worker 发生错误:", error.message);
            resultSpan.textContent = `Worker 错误: ${error.message}`;
            statusSpan.textContent = "错误。";
            loaderDiv.style.display = 'none';
            calculateButton.disabled = false;
            calculateButtonMainThread.disabled = false;
        };
    } else {
        console.warn("浏览器不支持 Web Workers。Worker 相关的计算将不可用。");
        calculateButton.disabled = true;
        statusSpan.textContent = "浏览器不支持 Web Worker。";
    }
    
    // --- 在主线程中计算斐波那契数列 (用于对比,会导致 UI 卡顿) ---
    function fibonacciMainThread(n) {
        if (n <= 0) return "输入必须为正整数";
        if (n <= 2) return 1;
        let a = 1, b = 1;
        for (let i = 3; i <= n; i++) {
            let temp = a + b;
            a = b;
            b = temp;
        }
        return b;
    }
    
    calculateButtonMainThread.addEventListener('click', () => {
        const n = parseInt(fibInput.value);
        if (isNaN(n) || n <= 0) {
            resultSpan.textContent = "请输入一个有效的正整数。";
            return;
        }
        console.log(`主线程: 开始在主线程计算斐波那契数 F(${n})`);
        resultSpan.textContent = "计算中 (主线程)...";
        statusSpan.textContent = `正在计算 F(${n}) (在主线程 - UI 可能无响应)...`;
        loaderDiv.style.display = 'inline-block';
        calculateButton.disabled = true;
        calculateButtonMainThread.disabled = true;
    
        // 使用 setTimeout 延迟执行,以便浏览器有机会更新 UI 显示 "计算中"
        // 但实际的 fibonacciMainThread 调用仍然会阻塞主线程。
        setTimeout(() => {
            const startTime = performance.now();
            const fibResult = fibonacciMainThread(n);
            const endTime = performance.now();
            console.log(`主线程: 主线程计算完成 F(${n}) = ${fibResult},耗时: ${(endTime - startTime).toFixed(2)}ms`);
            resultSpan.textContent = String(fibResult);
            statusSpan.textContent = `计算完成 (在主线程)。耗时: ${(endTime - startTime).toFixed(2)}ms`;
            loaderDiv.style.display = 'none';
            calculateButton.disabled = false;
            calculateButtonMainThread.disabled = false;
        }, 10); // 短暂延迟
    });
    
    console.log("主线程脚本 (main_fib.js) 初始化完毕。");
  • fib_worker.js (Worker 逻辑):

    js 复制代码
    // fib_worker.js
    console.log("斐波那契 Worker (fib_worker.js) 开始执行。");
    
    function calculateFibonacci(n) {
        console.log(`Worker: 开始计算斐波那契数 F(${n})`);
        if (n <= 0) return "输入必须为正整数";
        if (n <= 2) return 1;
        // 使用迭代法计算,递归对于大的 n 可能会导致栈溢出或效率低下
        let a = 1, b = 1;
        for (let i = 3; i <= n; i++) {
            let temp = a + b;
            a = b;
            b = temp;
            // 为了模拟更耗时的计算,可以加入一些无意义的循环
            // for (let j = 0; j < 1000; j++); // 谨慎使用,会显著增加计算时间
        }
        console.log(`Worker: F(${n}) 计算完成,结果为 ${b}`);
        return b;
    }
    
    self.onmessage = function(event) {
        const n = event.data;
        console.log(`Worker: 从主线程接收到计算请求 F(${n})`);
        const startTime = performance.now();
        const result = calculateFibonacci(n);
        const endTime = performance.now();
        console.log(`Worker: 计算 F(${n}) 耗时: ${(endTime - startTime).toFixed(2)}ms`);
    
        console.log(`Worker: 将结果 ${result} 发送回主线程。`);
        self.postMessage(result);
    };
    
    // 可以添加一个错误处理,例如,如果接收到的数据不是数字
    // self.addEventListener('message', function(event) {
    //     if (typeof event.data !== 'number' || event.data <= 0) {
    //         self.postMessage({ error: '无效的输入,请输入一个正整数。' });
    //         // 或者抛出错误
    //         // throw new Error('无效的输入,请输入一个正整数。');
    //         return;
    //     }
    //     const n = event.data;
    //     // ... 计算逻辑 ...
    //     self.postMessage(result);
    // });
    
    console.log("斐波那契 Worker (fib_worker.js) 已准备好。");

    代码讲解 (示例2):

    • index_fib.html:

      • 提供一个输入框让用户指定计算斐波那契数列的第 N 项。
      • 两个按钮:一个使用 Web Worker 计算,另一个直接在主线程计算(用于对比效果)。
      • 显示状态和结果的区域,以及一个加载动画。
      • 页面底部有一个输入框和一个按钮,用于测试在计算过程中 UI 是否仍然响应。
    • main_fib.js:

      • fibWorker = new Worker('fib_worker.js'); 创建 Worker。

      • 当点击 "开始计算 (使用 Worker)" 按钮时:

        • 更新 UI 显示为计算状态。
        • fibWorker.postMessage(n); 将要计算的数字 n 发送给 Worker。
      • fibWorker.onmessage: 接收 Worker 计算完成的结果,并更新 UI。

      • fibonacciMainThread(n): 这是一个在主线程中执行的斐波那契计算函数。

      • 当点击 "开始计算 (在主线程)" 按钮时:

        • 调用 fibonacciMainThread(n)。你会发现,当 n 较大时(例如 40 或更高),页面 UI 会在计算期间完全卡住,无法响应用户操作,直到计算完成。这是因为 JavaScript 的单线程特性。
        • 使用 setTimeout 只是为了让浏览器有机会在开始阻塞计算前更新一次UI(显示"计算中"),但实际计算仍然会阻塞。
    • fib_worker.js:

      • calculateFibonacci(n): 实际执行斐波那契计算的函数。
      • self.onmessage: 接收到主线程发送的数字 n 后,调用 calculateFibonacci(n),然后将结果通过 self.postMessage(result) 发回主线程。
      • 在 Worker 中执行这个耗时计算时,主线程的 UI 仍然是流畅和可交互的。

示例3:Worker 内部状态管理与多次通信

这个例子演示 Worker 如何维护内部状态,并根据主线程发送的不同命令执行不同操作。

  • index_counter.html:

    html 复制代码
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <title>Web Worker - 状态管理</title>
        <style>
            body { font-family: sans-serif; padding: 20px; }
            button { margin: 5px; padding: 10px; }
            #counterValue { font-size: 2em; font-weight: bold; margin: 20px 0; }
            #log { border: 1px solid #eee; padding: 10px; height: 150px; overflow-y: auto; margin-top: 20px; }
        </style>
    </head>
    <body>
        <h1>计数器 Worker</h1>
        <p>当前计数值: <span id="counterValue">0</span></p>
        <button id="incrementButton">增加</button>
        <button id="decrementButton">减少</button>
        <button id="resetButton">重置 (值为 100)</button>
        <button id="getButton">获取当前值</button>
        <div id="log">操作日志:</div>
        <script src="main_counter.js"></script>
    </body>
    </html>
  • main_counter.js:

    js 复制代码
    // main_counter.js
    console.log("主线程 (main_counter.js) 启动");
    
    const counterValueSpan = document.getElementById('counterValue');
    const incrementButton = document.getElementById('incrementButton');
    const decrementButton = document.getElementById('decrementButton');
    const resetButton = document.getElementById('resetButton');
    const getButton = document.getElementById('getButton');
    const logDiv = document.getElementById('log');
    
    function addToLog(message) {
        const p = document.createElement('p');
        p.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
        logDiv.appendChild(p);
        logDiv.scrollTop = logDiv.scrollHeight; // 自动滚动到底部
        console.log("LOG:", message);
    }
    
    if (window.Worker) {
        const counterWorker = new Worker('counter_worker.js');
        addToLog("计数器 Worker 已创建。");
    
        // --- 向 Worker 发送命令 ---
        incrementButton.addEventListener('click', () => {
            addToLog("主线程: 发送 'increment' 命令。");
            counterWorker.postMessage({ command: 'increment' });
        });
    
        decrementButton.addEventListener('click', () => {
            addToLog("主线程: 发送 'decrement' 命令。");
            counterWorker.postMessage({ command: 'decrement' });
        });
    
        resetButton.addEventListener('click', () => {
            const resetValue = 100;
            addToLog(`主线程: 发送 'reset' 命令,值为 ${resetValue}。`);
            counterWorker.postMessage({ command: 'reset', value: resetValue });
        });
    
        getButton.addEventListener('click', () => {
            addToLog("主线程: 发送 'get' 命令。");
            counterWorker.postMessage({ command: 'get' });
        });
    
        // --- 从 Worker 接收响应 ---
        counterWorker.onmessage = function(event) {
            const response = event.data;
            console.log("主线程: 从 Worker 接收到响应:", response);
    
            if (response && typeof response.currentValue !== 'undefined') {
                counterValueSpan.textContent = response.currentValue;
                addToLog(`Worker 响应: 当前值为 ${response.currentValue} (来自 ${response.sourceCommand} 命令)。`);
            } else if (response && response.message) {
                addToLog(`Worker 消息: ${response.message}`);
            } else {
                addToLog(`Worker 原始响应: ${JSON.stringify(response)}`);
            }
        };
    
        counterWorker.onerror = function(error) {
            const errorMessage = `Worker 错误: ${error.message} 在 ${error.filename}:${error.lineno}`;
            addToLog(errorMessage);
            console.error(errorMessage);
        };
    
        // 初始化时获取一次当前值
        addToLog("主线程: 初始化,发送 'get' 命令获取初始值。");
        counterWorker.postMessage({ command: 'get' });
    
    } else {
        addToLog("你的浏览器不支持 Web Workers。");
        console.warn("浏览器不支持 Web Workers。");
        incrementButton.disabled = true;
        decrementButton.disabled = true;
        resetButton.disabled = true;
        getButton.disabled = true;
    }
  • counter_worker.js:

    js 复制代码
    // counter_worker.js
    console.log("计数器 Worker (counter_worker.js) 启动");
    
    let counter = 0; // Worker 内部维护的状态
    
    console.log(`Worker: 初始计数值为 ${counter}`);
    
    self.onmessage = function(event) {
        const message = event.data;
        if (!message || !message.command) {
            console.warn("Worker: 收到无效命令格式:", message);
            self.postMessage({ error: "无效命令", received: message });
            return;
        }
    
        const command = message.command;
        console.log(`Worker: 收到命令 "${command}"`, message);
    
        switch (command) {
            case 'increment':
                counter++;
                console.log(`Worker: 'increment' 执行后,计数值为 ${counter}`);
                self.postMessage({ currentValue: counter, sourceCommand: 'increment' });
                break;
            case 'decrement':
                counter--;
                console.log(`Worker: 'decrement' 执行后,计数值为 ${counter}`);
                self.postMessage({ currentValue: counter, sourceCommand: 'decrement' });
                break;
            case 'reset':
                if (typeof message.value === 'number') {
                    counter = message.value;
                    console.log(`Worker: 'reset' 执行后,计数值重置为 ${counter}`);
                    self.postMessage({ currentValue: counter, sourceCommand: 'reset' });
                } else {
                    console.warn("Worker: 'reset' 命令缺少有效的 value 参数。");
                    self.postMessage({ error: "'reset' 命令需要一个数字类型的 'value'。", sourceCommand: 'reset', currentValue: counter });
                }
                break;
            case 'get':
                console.log(`Worker: 'get' 命令,当前计数值为 ${counter}`);
                self.postMessage({ currentValue: counter, sourceCommand: 'get' });
                break;
            default:
                console.warn(`Worker: 未知命令 "${command}"`);
                self.postMessage({ error: `未知命令: ${command}`, sourceCommand: command, currentValue: counter });
        }
    };
    
    // 可以在这里添加一个初始消息,告知主线程 Worker 已准备就绪
    // self.postMessage({ message: "计数器 Worker 已准备就绪并初始化。" });
    // 但通常主线程会在创建 Worker 后立即发送第一个 'get' 命令来获取初始状态
    
    console.log("计数器 Worker (counter_worker.js) 已准备好接收命令。");

    代码讲解 (示例3):

    • index_counter.html: 提供了增加、减少、重置和获取计数器值的按钮,以及一个显示当前值和操作日志的区域。

    • main_counter.js:

      • 创建 counter_worker.js
      • 为每个按钮添加事件监听器。当按钮被点击时,它会构造一个包含 command 属性的对象(例如 { command: 'increment' }{ command: 'reset', value: 100 }),并通过 counterWorker.postMessage() 发送给 Worker。
      • counterWorker.onmessage 用于接收来自 Worker 的响应。Worker 的响应通常也包含一个对象,其中有 currentValue 属性表示当前的计数值,以及 sourceCommand 来指明这是对哪个命令的响应。
      • addToLog 函数用于在页面上记录操作。
    • counter_worker.js:

      • let counter = 0;:在 Worker 内部定义一个变量 counter 来存储计数值。这个变量的状态在 Worker 的生命周期内被保持。

      • self.onmessage: 接收主线程发送的命令对象。

      • 使用 switch 语句根据 message.command 的值来执行不同的操作:

        • 'increment': counter++
        • 'decrement': counter--
        • 'reset': 将 counter 设置为 message.value
        • 'get': 不改变 counter,仅返回当前值。
      • 在执行完每个命令后,Worker 通过 self.postMessage({ currentValue: counter, sourceCommand: command }) 将更新后的计数值和原始命令发送回主线程。

这个示例清晰地展示了 Worker 如何作为一种独立的服务运行,管理自己的状态,并响应来自主线程的指令。

示例4:使用 Transferable Objects 传递 ArrayBuffer

此示例演示如何高效地在主线程和 Worker 之间传递大量二进制数据 (ArrayBuffer),利用可转移对象的特性避免数据拷贝。

  • index_transferable.html:

    js 复制代码
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <title>Web Worker - Transferable Objects</title>
        <style>
            body { font-family: sans-serif; padding: 20px; }
            button { padding: 10px 15px; margin-top: 10px; }
            #log { margin-top: 20px; border: 1px solid #ccc; padding: 10px; height: 300px; overflow-y: auto; }
            p { margin: 5px 0; }
        </style>
    </head>
    <body>
        <h1>Transferable Objects 示例 (ArrayBuffer)</h1>
        <p>此示例将创建一个 ArrayBuffer,填充数据,然后将其"转移"给 Worker。Worker 修改数据后再将其"转移"回主线程。</p>
        <button id="createAndSendButton">创建并发送 ArrayBuffer (转移)</button>
        <button id="createAndSendCopyButton">创建并发送 ArrayBuffer (拷贝)</button>
        <div id="log">操作日志:</div>
    
        <script src="main_transferable.js"></script>
    </body>
    </html>
  • main_transferable.js:

    js 复制代码
    // main_transferable.js
    console.log("主线程 (main_transferable.js) 启动");
    
    const createAndSendButton = document.getElementById('createAndSendButton');
    const createAndSendCopyButton = document.getElementById('createAndSendCopyButton');
    const logDiv = document.getElementById('log');
    
    function addToLog(message, isError = false) {
        const p = document.createElement('p');
        p.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
        if (isError) p.style.color = 'red';
        logDiv.appendChild(p);
        logDiv.scrollTop = logDiv.scrollHeight;
        if (isError) console.error("LOG:", message); else console.log("LOG:", message);
    }
    
    if (window.Worker) {
        const transferableWorker = new Worker('transferable_worker.js');
        addToLog("Transferable Worker 已创建。");
    
        function sendBuffer(transfer) {
            const bufferSize = 1024 * 1024 * 8; // 8MB
            let arrayBuffer = new ArrayBuffer(bufferSize);
            let uint8View = new Uint8Array(arrayBuffer);
    
            addToLog(`主线程: 创建了一个 ${bufferSize / (1024 * 1024)}MB 的 ArrayBuffer。`);
            addToLog(`主线程: 发送前,ArrayBuffer.byteLength = ${arrayBuffer.byteLength}`);
            // 填充一些数据
            for (let i = 0; i < 100; i++) { // 只填充前100个字节作为示例
                uint8View[i] = i;
            }
            addToLog(`主线程: 填充了初始数据,例如 view[0]=${uint8View[0]}, view[99]=${uint8View[99]}`);
    
            const action = transfer ? 'transfer' : 'copy';
            const messagePayload = {
                action: action,
                buffer: arrayBuffer
            };
    
            try {
                if (transfer) {
                    addToLog("主线程: 准备使用 postMessage 转移 ArrayBuffer...");
                    transferableWorker.postMessage(messagePayload, [arrayBuffer]); // 第二个参数指定要转移的对象
                } else {
                    addToLog("主线程: 准备使用 postMessage 拷贝 ArrayBuffer...");
                    transferableWorker.postMessage(messagePayload); // 不指定第二个参数,默认拷贝
                }
                addToLog(`主线程: 发送后,ArrayBuffer.byteLength = ${arrayBuffer.byteLength} (如果是转移,这里应为0)`);
                if (arrayBuffer.byteLength === 0 && transfer) {
                    addToLog("主线程: ArrayBuffer 已成功转移,主线程无法再访问其内容。");
                } else if (arrayBuffer.byteLength > 0 && !transfer) {
                     addToLog("主线程: ArrayBuffer 已被拷贝,主线程仍保留其副本。");
                }
    
                // 尝试访问被转移的 ArrayBuffer 会导致问题或得到空数据
                // if (transfer && arrayBuffer.byteLength === 0) {
                //     try {
                //         let testView = new Uint8Array(arrayBuffer); // 这可能不会立即报错,但内容已丢失
                //         addToLog(`主线程: 尝试访问已转移的 buffer, view[0] = ${testView[0]}`); // 通常是 undefined 或 0
                //     } catch (e) {
                //         addToLog(`主线程: 访问已转移的 buffer 时出错: ${e.message}`, true);
                //     }
                // }
    
            } catch (e) {
                addToLog(`主线程: postMessage 时发生错误: ${e.message}`, true);
                // 如果浏览器不支持 Transferable Objects 但尝试转移,可能会在这里捕获到错误
                // 或者如果 arrayBuffer 已经被转移过,再次尝试转移也会报错。
            }
        }
    
        createAndSendButton.addEventListener('click', () => sendBuffer(true));
        createAndSendCopyButton.addEventListener('click', () => sendBuffer(false));
    
        transferableWorker.onmessage = function(event) {
            const response = event.data;
            addToLog("主线程: 从 Worker 接收到响应。");
    
            if (response.buffer) {
                let receivedBuffer = response.buffer;
                addToLog(`主线程: 接收到的 ArrayBuffer.byteLength = ${receivedBuffer.byteLength}`);
                if (receivedBuffer.byteLength > 0) {
                    let receivedView = new Uint8Array(receivedBuffer);
                    addToLog(`主线程: Worker 修改后的数据示例: view[0]=${receivedView[0]}, view[1]=${receivedView[1]}`);
                    addToLog("主线程: Worker 已将 ArrayBuffer 的所有权转回。");
                } else {
                    addToLog("主线程: 接收到的 ArrayBuffer 为空,可能在 Worker 端转移时出现问题或已被再次转移。", true);
                }
            } else if (response.message) {
                addToLog(`主线程: Worker 消息: ${response.message}`);
            } else if (response.error) {
                addToLog(`主线程: Worker 错误: ${response.error}`, true);
            }
        };
    
        transferableWorker.onerror = function(error) {
            addToLog(`主线程: Worker 发生错误: ${error.message} 在 ${error.filename}:${error.lineno}`, true);
        };
    
    } else {
        addToLog("你的浏览器不支持 Web Workers。", true);
        createAndSendButton.disabled = true;
        createAndSendCopyButton.disabled = true;
    }
  • transferable_worker.js:

    js 复制代码
    // transferable_worker.js
    console.log("Transferable Worker (transferable_worker.js) 启动");
    
    self.onmessage = function(event) {
        const payload = event.data;
        if (!payload || !payload.buffer || !(payload.buffer instanceof ArrayBuffer)) {
            console.error("Worker: 收到无效数据,期望一个包含 ArrayBuffer 的对象。");
            self.postMessage({ error: "无效数据格式" });
            return;
        }
    
        const receivedBuffer = payload.buffer;
        const action = payload.action; // 'transfer' or 'copy'
    
        console.log(`Worker: 收到来自主线程的 ArrayBuffer,操作类型: ${action}`);
        console.log(`Worker: 接收时 ArrayBuffer.byteLength = ${receivedBuffer.byteLength}`);
    
        if (receivedBuffer.byteLength === 0) {
            console.warn("Worker: 接收到的 ArrayBuffer 为空,可能在主线程发送时已失效或转移未成功。");
            self.postMessage({ error: "Worker 接收到的 ArrayBuffer 为空。" });
            return;
        }
    
        try {
            let uint8View = new Uint8Array(receivedBuffer);
            console.log(`Worker: 接收到的数据示例: view[0]=${uint8View[0]}, view[99]=${uint8View[99]}`);
    
            // 修改 ArrayBuffer 中的数据
            console.log("Worker: 正在修改 ArrayBuffer 中的数据...");
            for (let i = 0; i < Math.min(uint8View.length, 10); i++) { // 修改前10个字节
                uint8View[i] = uint8View[i] + 100; // 例如,每个字节加100
            }
            console.log(`Worker: 修改后的数据示例: view[0]=${uint8View[0]}, view[1]=${uint8View[1]}`);
    
            // 将修改后的 ArrayBuffer 发送回主线程
            // 同样,我们可以选择转移或拷贝
            // 为了演示,这里总是尝试转移回去
            console.log("Worker: 准备将修改后的 ArrayBuffer 转移回主线程。");
            console.log(`Worker: 发送回主线程前,ArrayBuffer.byteLength = ${receivedBuffer.byteLength}`);
    
            // 构造响应对象
            const responsePayload = {
                message: `Buffer processed by worker (action: ${action})`,
                buffer: receivedBuffer
            };
    
            self.postMessage(responsePayload, [receivedBuffer]); // 转移 receivedBuffer
            console.log(`Worker: 发送回主线程后,ArrayBuffer.byteLength = ${receivedBuffer.byteLength} (应为0)`);
    
            if (receivedBuffer.byteLength === 0) {
                 console.log("Worker: ArrayBuffer 已成功转移回主线程。");
            }
    
        } catch (e) {
            console.error("Worker: 处理 ArrayBuffer 时发生错误:", e.message, e.stack);
            self.postMessage({ error: `Worker 内部错误: ${e.message}` });
        }
    };
    
    console.log("Transferable Worker (transferable_worker.js) 已准备就绪。");

    代码讲解 (示例4):

    • index_transferable.html : 提供了两个按钮,一个用于通过"转移"(Transferable)方式发送 ArrayBuffer,另一个通过传统的"拷贝"方式发送。日志区域会显示操作过程和结果。

    • main_transferable.js:

      • sendBuffer(transfer) 函数:

        • 创建一个 8MB 大小的 ArrayBuffer 并填充一些初始数据。

        • 记录发送前 arrayBuffer.byteLength

        • 根据 transfer 参数的值,决定是转移还是拷贝 ArrayBuffer

          • 转移 : transferableWorker.postMessage(messagePayload, [arrayBuffer]);。第二个参数 [arrayBuffer] 告诉浏览器将 arrayBuffer 对象的所有权转移给 Worker。转移后,主线程中的 arrayBuffer.byteLength 会变为 0,并且主线程不能再访问其内容。 [15]
          • 拷贝 : transferableWorker.postMessage(messagePayload);。不提供第二个参数,arrayBuffer 会被结构化克隆一份副本发送给 Worker,主线程中的原始 arrayBuffer 保持不变。
        • 记录发送后 arrayBuffer.byteLength 以验证转移或拷贝的效果。

      • transferableWorker.onmessage: 接收 Worker 处理并返回的 ArrayBuffer。Worker 也会将 ArrayBuffer 转移回来。主线程接收到后,可以检查其内容和长度。

    • transferable_worker.js:

      • self.onmessage: 接收主线程发送的包含 ArrayBuffer 的消息。
      • 记录接收时 receivedBuffer.byteLength
      • ArrayBuffer 中的数据进行一些修改(例如,将每个字节的值增加)。
      • 通过 self.postMessage(responsePayload, [receivedBuffer]); 将修改后的 ArrayBuffer 转移 回主线程。同样,在 Worker 中,转移后的 receivedBuffer.byteLength 也会变为 0。

    对比转移和拷贝

    • 拷贝 :对于大数据量的 ArrayBuffer,拷贝操作会消耗显著的时间和内存,因为需要创建数据的完整副本。
    • 转移 :几乎是瞬时的,因为它不涉及数据复制,只是所有权的转移。这对于性能敏感的应用(如实时音视频处理、大型文件操作)至关重要。 [15][16]
    • 注意 :一旦对象被转移,原始上下文中的该对象将不再可用。 [17]

示例5:Worker 错误处理与终止 (已在示例1中部分演示)

我们回顾一下示例1中关于错误处理和终止的部分,并可以进一步细化。

  • 主线程的 worker.onerror:

    js 复制代码
    // main.js (部分)
    myWorker.onerror = function(errorEvent) {
        console.error('主线程捕获到 Worker 错误:');
        console.error('  消息:', errorEvent.message);
        console.error('  文件名:', errorEvent.filename);
        console.error('  行号:', errorEvent.lineno);
        // errorEvent.preventDefault(); // 可选,阻止默认的控制台错误输出
        // 更新UI,告知用户Worker出错了
        outputDiv.textContent = `Worker 发生严重错误: ${errorEvent.message}`;
    };

    如果 Worker 脚本中有一个未被 try...catch 捕获的错误(例如,引用一个未定义的变量,或者像示例1中那样访问 null 的属性),这个 onerror 处理函数就会被触发。 [2]

  • Worker 内部的 try...catch:

    js 复制代码
    // worker.js (部分)
    self.onmessage = function(event) {
        try {
            if (event.data === 'provoke_error') {
                let a = undefined_variable; // 这会抛出 ReferenceError
            }
            // ... 其他逻辑 ...
            self.postMessage("任务成功完成");
        } catch (e) {
            console.error("Worker 内部捕获到错误:", e.name, e.message);
            // 主动将错误信息发送回主线程
            self.postMessage({ type: 'error', name: e.name, message: e.message, stack: e.stack });
        }
    };

    在 Worker 内部使用 try...catch 可以更精细地控制错误处理,并允许 Worker 将格式化的错误信息发送回主线程,而不是仅仅依赖主线程的 onerror

  • 终止 Worker:

    • 从主线程终止: myWorker.terminate(); (如示例1所示)。这会立即杀死 Worker 线程,Worker 内部的任何进行中的操作(包括 finally 块或异步操作)都不会完成。 [1]

    • Worker 自我终止: self.close();

      js 复制代码
      // worker.js (部分)
      self.onmessage = function(event) {
          if (event.data === 'finish_and_close') {
              console.log("Worker: 收到关闭命令,正在完成最后的工作...");
              // 执行一些清理工作...
              self.postMessage("Worker 已完成所有任务并即将关闭。");
              console.log("Worker: 自行关闭。");
              self.close(); // Worker 自行终止
              // self.close() 之后,此 Worker 实例将不再响应任何消息
              // 尝试在此之后执行 console.log 可能不会生效,因为线程已停止
          }
      };

      self.close() 是一种更优雅的关闭方式,允许 Worker 在退出前完成一些清理工作。 [1]

示例6:在 Worker 中使用 importScripts

此示例演示 Worker 如何加载和使用外部 JavaScript 文件。

  • index_import.html:

    xml 复制代码
    <!DOCTYPE html>
    <html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <title>Web Worker - importScripts</title>
    </head>
    <body>
        <h1>importScripts 示例</h1>
        <button id="runWorkerButton">运行 Worker (使用导入的脚本)</button>
        <div id="output">等待 Worker 响应...</div>
        <script src="main_import.js"></script>
    </body>
    </html>
  • main_import.js:

    js 复制代码
    // main_import.js
    const runWorkerButton = document.getElementById('runWorkerButton');
    const outputDiv = document.getElementById('output');
    
    if (window.Worker) {
        const importWorker = new Worker('import_worker.js');
    
        runWorkerButton.addEventListener('click', () => {
            outputDiv.textContent = "主线程: 发送任务给 Worker...";
            importWorker.postMessage({ task: "calculateSum", data: [10, 20, 30] });
        });
    
        importWorker.onmessage = function(event) {
            outputDiv.textContent = `主线程: Worker 响应: ${JSON.stringify(event.data)}`;
        };
    
        importWorker.onerror = function(error) {
            outputDiv.textContent = `主线程: Worker 错误: ${error.message}`;
            console.error("Worker error:", error);
        };
    } else {
        outputDiv.textContent = "浏览器不支持 Web Workers。";
    }
  • helper_math.js (被导入的脚本1):

    js 复制代码
    // helper_math.js
    console.log("helper_math.js 正在被 Worker 加载和执行...");
    
    function add(a, b) {
        console.log("helper_math.js: add() 被调用");
        return a + b;
    }
    
    function sumArray(arr) {
        console.log("helper_math.js: sumArray() 被调用");
        return arr.reduce((acc, val) => acc + val, 0);
    }
    
    const MATH_CONSTANT = 3.14159;
    
    // 故意不导出,因为 importScripts 是将脚本内容直接注入全局作用域
  • helper_string.js (被导入的脚本2):

    js 复制代码
    // helper_string.js
    console.log("helper_string.js 正在被 Worker 加载和执行...");
    
    function greet(name) {
        console.log("helper_string.js: greet() 被调用");
        return `你好, ${name}! (来自 helper_string.js)`;
    }
  • import_worker.js (使用 importScripts 的 Worker):

    js 复制代码
    // import_worker.js
    console.log("import_worker.js 开始执行。");
    
    try {
        console.log("Worker: 准备导入外部脚本...");
        importScripts('helper_math.js', 'helper_string.js');
        // 如果 helper_math.js 或 helper_string.js 不存在或加载失败,会抛出错误
        // 并且后续代码(包括 onmessage 设置)可能不会执行
    
        console.log("Worker: 外部脚本导入成功。");
        console.log("Worker: MATH_CONSTANT 的值:", MATH_CONSTANT); // 可以直接访问
    
        self.onmessage = function(event) {
            const request = event.data;
            console.log("Worker: 收到消息:", request);
    
            if (request.task === "calculateSum" && Array.isArray(request.data)) {
                const sum = sumArray(request.data); // 调用来自 helper_math.js 的函数
                console.log(`Worker: sumArray(${request.data}) = ${sum}`);
                const greeting = greet("主线程"); // 调用来自 helper_string.js 的函数
                self.postMessage({ result: sum, message: greeting });
            } else {
                self.postMessage({ error: "未知的任务或无效数据" });
            }
        };
    
        console.log("Worker: onmessage 处理函数已设置。");
    
    } catch (e) {
        console.error("Worker: 导入脚本或初始化时发生错误:", e.message, e.stack);
        // 如果 importScripts 失败,可能需要一种方式通知主线程
        // 但此时 onmessage 可能还未设置,所以主线程的 onerror 是主要的错误捕获点
        // 可以尝试发送一个错误消息,但这不保证能成功
        self.postMessage({ type: 'INIT_ERROR', message: `Worker 初始化失败: ${e.message}` });
        // 或者直接抛出,让主线程的 onerror 捕获
        // throw new Error(`Worker 初始化失败: ${e.message}`);
    }
    
    console.log("import_worker.js 执行完毕。");

    代码讲解 (示例6):

    • helper_math.jshelper_string.js: 这两个文件包含一些辅助函数和变量。它们就像普通的 JavaScript 文件。

    • import_worker.js:

      • importScripts('helper_math.js', 'helper_string.js');: 这一行是关键。它会同步 下载并执行 helper_math.jshelper_string.js 文件。 [20][21]
      • 执行后,这两个文件中定义的全局函数(如 sumArray, greet)和变量(如 MATH_CONSTANT)会直接添加到 Worker 的全局作用域中,因此可以在 import_worker.js 中直接调用它们。
      • try...catch 块用于捕获 importScripts 可能发生的错误(例如,文件未找到或脚本内部有语法错误)。
      • self.onmessage 处理函数接收主线程的任务请求,并调用从导入脚本中获得的函数来完成任务。
    • main_import.js: 负责创建 Worker,发送任务数据,并接收和显示结果或错误。

    使用 importScripts 可以帮助组织 Worker 代码,将功能模块化到不同的文件中。但需要注意它是同步加载的,如果导入的脚本很大或网络慢,会阻塞 Worker 的初始化。 [8] 对于现代开发,如果 Worker 脚本本身是作为 ES 模块 (type: 'module') 加载的,那么应该在 Worker 内部使用 ES6 的 import 语句来导入其他模块。

第四部分:总结与最佳实践

  1. 何时使用 Web Worker?

    • CPU 密集型计算 :如复杂的数学运算、数据分析、密码学操作、图像/音频/视频处理(例如,在将数据发送到 GPU 之前进行预处理)。 [3][4]
    • 后台数据同步/预取:在后台静默地从服务器获取数据或向服务器发送数据,而不会影响用户当前的交互。
    • 保持 UI 响应 :任何可能导致主线程长时间无响应的任务都应该考虑放到 Worker 中。 [22]
    • 大型数据集处理:例如,对大型 JSON 对象进行解析和转换,或者处理 IndexedDB 中的大量数据。
  2. 注意事项

    • 通信开销postMessage 虽然高效,但仍然涉及数据的序列化和反序列化(结构化克隆)。对于非常频繁或非常小量的消息,其开销可能相对较高。对于大数据,应优先考虑 Transferable Objects。 [3][12]
    • 内存占用 :每个 Worker 都是一个独立的线程,会消耗额外的内存和系统资源。避免创建过多的 Worker。 [1]
    • 同源限制 :Worker 脚本必须与主页面同源。 [1]
    • 无法直接操作 DOM :所有 UI 更新必须通过 postMessage 将数据发送回主线程,由主线程来操作 DOM。 [22]
    • 脚本加载importScripts() 是同步的,可能会阻塞 Worker 的启动。考虑使用 ES 模块 Worker 以利用异步 import
    • 错误处理 :务必在主线程和 Worker 内部都设置完善的错误处理机制。 [2][5]
    • 终止 Worker :在 Worker 不再需要时,务必使用 worker.terminate()self.close() 来终止它,以释放资源。 [1]
  3. 调试技巧

    • 现代浏览器的开发者工具通常都支持调试 Web Worker。你可以在 "Sources" (Chrome) 或 "Debugger" (Firefox) 面板中找到 Worker 脚本,设置断点,单步执行代码,并查看变量。 [2]
    • 在 Worker 内部大量使用 console.log() 来追踪执行流程和数据状态。
    • 确保 Worker 的 onerror 和主线程的 worker.onerror 都被正确设置,以便捕获和报告错误。

Web Worker 是一个强大的工具,可以显著提升 Web 应用的性能和响应性。通过理解其工作原理、通信机制和限制,并结合实际场景选择合适的应用方式,可以有效地利用它来构建更流畅、更高效的用户体验。


好文:

  1. Web Worker 使用教程- 阮一峰的网络日志
  2. 使用Web Worker - Web API - MDN Web Docs
  3. 浅谈HTML5 Web Worker,性能优化利器? - 开发者客栈
  4. Web Worker 性能优化初体验 - 腾讯云
  5. 利用HTML5 Web Worker实现流畅的数据处理和计算
  6. 最佳的Web Worker教程 - ILLA Cloud
  7. JavaScript 工作原理:Web Worker 的内部构造以及5 种你应当使用它的场景 - GitHub
  8. Web Worker 试玩 - Lewin's Blog
  9. 在Vue中使用Web Worker详细教程原创 - CSDN博客
  10. WebWorker:工作者线程初探- SuanYunyan - 博客园
  11. Worker.postMessage() - Web API | MDN
  12. 深入浅出:Web Workers 与SharedArrayBuffer 的性能优化 - 稀土掘金
  13. Web Worker 详解 - 一叶斋
  14. 详解Web Worker,不再止步于会用! - 稀土掘金
  15. 可转移的对象- 闪电般的速度| Blog - Chrome for Developers
  16. 可轉移的物件- Transferable objects - iT 邦幫忙
  17. Transferable objects - Web APIs | MDN
  18. 可转移对象- Web API
  19. 一文彻底学会使用web worker众所周知,Javascript最初设计是运行在浏览器中的,为了防止多个线程同时操作D - 稀土掘金
  20. 专用工作者线程worker加载模块方法,import引入和importscirpt引入原创 - CSDN博客
  21. jsjsjs 第1081天在js中importScripts方法有什么作用? #5025 - GitHub
  22. JavaScript 性能优化- 学习Web 开发| MDN
相关推荐
Data_Adventure10 分钟前
Vite 项目中使用 vite-plugin-dts 插件的详细指南
前端·vue.js
八戒社14 分钟前
如何使用插件和子主题添加WordPress自定义CSS(附:常见错误)
前端·css·tensorflow·wordpress
xzboss26 分钟前
DOM转矢量PDF
前端·javascript
一无所有不好吗26 分钟前
纯前端vue项目实现版本更新(纯代码教程)
前端
安全系统学习40 分钟前
内网横向之RDP缓存利用
前端·安全·web安全·网络安全·中间件
Hilaku1 小时前
为什么我不再相信 Tailwind?三个月重构项目教会我的事
前端·css·前端框架
FogLetter1 小时前
JavaScript 的历史:从网页点缀到改变世界的编程语言
前端·javascript·http
鹏北海1 小时前
Vue3+TS的H5项目实现微信分享卡片样式
前端·微信
轻颂呀1 小时前
进程——环境变量及程序地址空间
前端·chrome
lyc2333331 小时前
鸿蒙Stage模型:轻量高效的应用架构「舞台革命」🎭
前端