一、Web Worker 的核心特性

Web Worker 是 HTML5 标准的一部分。这套 API 让开发者可以在主线程之外开辟新的 Worker 线程,并在其中运行一段 JavaScript 脚本,真正赋予了前端操作多线程的能力。它的核心特性包括:

  • 独立线程:每个 Worker 运行在自己的线程中,拥有独立的事件循环机制、内存空间和任务队列。
  • 与 DOM 完全隔离:Worker 内部无法访问 documentwindow 等浏览器全局对象,不能直接操作页面元素。
  • 可用的 Web API:虽然不能碰 DOM,但 Worker 中依然可以使用大量异步 API,例如:fetchXMLHttpRequestsetTimeoutPromiseIndexedDB 等,这为后台数据处理、预缓存等场景提供了很大的灵活性。
  • 通信靠消息:主线程与 Worker 之间通过 postMessage 发送数据,通过 onmessage 或 addEventListener('message', ...) 接收数据,无法直接引用对方的内存。这种做法带来了天然的线程安全——既然没有共享可变状态,自然就不会有锁竞争和数据覆盖的问题。
  • 数据传递:从深拷贝到所有权转移:默认情况下,postMessage 会对传递的数据进行结构化克隆(深拷贝)。可以拷贝的数据类型很丰富:字符串、数字、对象、数组、MapSetArrayBuffer 等。但对于大型二进制数据(如一段 10MB 的 ArrayBuffer),深拷贝的开销会很大;我们可以使用可转移对象(Transferable)。通过在 postMessage 的第二个参数中指定可转移对象,数据的所有权会从发送方直接转移给接收方(转移后,原线程中的 buffer 会进入 detached 状态,无法再被使用),实现近乎零成本的传递。

二、Worker 的基本用法

2.1 检查支持

在正式开始使用前,最好先检测一下当前环境是否支持 Web Worker:

if (typeof Worker !== "undefined") {
  // 支持 Web Worker
} else {
  // 不支持,回退方案
}

2.2 Worker 的创建与终止

创建一个 Worker 实例,需要传入一个 JavaScript 文件的路径及可选参数:

const worker = new Worker(path, options);

path 和 options 含义如下:

  • path:有效的 JS 脚本的地址,必须遵守同源策略。
  • options.type:可选,用于决定 Worker 脚本的加载方式。"classic" 为默认值,使用传统脚本模式;"module" 则为 ES 模块模式,支持顶层 import 和 export,更适合现代工程化项目。
  • options.credentials:可选,用于控制跨域请求的凭证携带,可选值 "omit""same-origin"(默认值)和 "include"
  • options.name:可选,允许你为 Worker 实例设置一个可读的标识名称,主要用于调试目的,在 Chrome DevTools 的 Sources 面板中能够识别。

如果不想创建单独的文件,还可以通过 Blob URL 动态生成 Worker 代码:

const code = `self.onmessage = (e) => { self.postMessage(e.data * 2); }`;
const blob = new Blob([code], { type: 'application/JavaScript' });
const worker = new Worker(URL.createObjectURL(blob));

当不再需要 Worker 时,可以调用 worker.terminate() 立即终止 Worker 线程,释放资源。

2.3 线程间数据传递

主线程与 Worker 线程都可以通过 postMessage 方法来发送消息,然后通过监听 message 事件来接收消息。主线程和 Worker 之间的通信模式是对称的。

主线程

const myWorker = new Worker('worker.js');

// 接收 Worker 发来的消息
myWorker.addEventListener('message', (e) => {
  console.log('来自 Worker:', e.data);
});

// 向 Worker 发送消息
myWorker.postMessage('Greeting from Main.js');

Worker 线程(worker.js)

// 接收主线程发来的消息
self.onmessage = (e) => {
  console.log('来自主线程:', e.data);

  // 执行计算...

  // 处理完后回复
  self.postMessage('Hello from Worker');
};

主要流程为:

  • 主线程:通过 new Worker(url) 加载一个 JS 文件来创建一个 Worker,同时返回一个 Worker 实例;通过 worker.postMessage(data) 向 Worker 发送数据;绑定 worker.onmessage 或 addEventListener('message', ...) 接收 Worker 发回的数据。
  • Worker 新线程:绑定 onmessage 或使用 addEventListener('message', ...) 接收主线程发送过来的数据;通过 postMessage(data) 将处理结果发送回主线程。

值得注意的是,如果同一个计算过程只是参数不同,完全可以重复使用同一个 Worker 实例,而不必每次都新建。这样可以避免反复加载脚本、初始化执行环境的开销,提升整体性能。

三、将 Worker 异步操作封装为 Promise

原生的 Worker 通信基于回调(onmessage),在多个任务并发、串行依赖等场景下容易陷入“回调地狱”。更好的做法是把每个 Worker 任务变成一个 Promise,然后用 async/await 优雅处理。

下面两部分代码是对浏览器 Web Worker 的一套轻量级封装,将 Worker 的双向消息通信包装成 基于 Promise 的任务调度模型,让开发者可以像调用异步函数一样使用 Worker,而无需手写消息监听与匹配逻辑。整套封装分为 主线程 和 Worker 线程 两部分,二者通过约定好的消息格式协同工作。

3.1 主线程部分

主线程的核心是一个 TaskProcessor 构造函数,以及与之配合的几个辅助函数。

function TaskProcessor(workerPath) {
  this._workerPath = workerPath;
  this._nextID = 0;
}

// 为某个具体任务创建消息监听器,通过 id 匹配请求与响应
const createOnmessageHandler = (worker, id, resolve, reject) => {
  const listener = ({ data }) => {
    if (data.id !== id) {
      return; // 不是自己发出的任务,忽略
    }

    if (data.error !== undefined) {
      reject(data.error);
    } else {
      resolve(data.result);
    }

    // 匹配成功后移除监听器,避免内存泄漏
    worker.removeEventListener("message", listener);
  };

  return listener;
};

const emptyTransferableObjectArray = [];

async function runTask(processor, parameters, transferableObjects) {
  if (transferableObjects === undefined) {
    transferableObjects = emptyTransferableObjectArray;
  }

  const id = processor._nextID++;
  const promise = new Promise((resolve, reject) => {
    processor._worker.addEventListener(
      "message",
      createOnmessageHandler(processor._worker, id, resolve, reject),
    );
  });

  processor._worker.postMessage(
    {
      id: id,
      parameters: parameters,
    },
    transferableObjects,
  );

  return promise;
}

async function scheduleTask(processor, parameters, transferableObjects) {
  try {
    const result = await runTask(processor, parameters, transferableObjects);
    return result;
  } catch (error) {
    throw error;
  }
}

TaskProcessor.prototype.scheduleTask = function (
  parameters,
  transferableObjects,
) {
  if (this._worker === undefined) {
    const options = {};
    // 如有需要可设为 options.type = "module";
    this._worker = new Worker(this._workerPath, options);
  }

  return scheduleTask(this, parameters, transferableObjects);
};

TaskProcessor.prototype.destroy = function () {
  if (this._worker !== undefined) {
    this._worker.terminate();
    this._worker = null;
  }
  // 其他清理逻辑可在此补充
};

1. 构造函数 TaskProcessor(workerPath)

  • workerPath:Worker 脚本的路径。
  • 维护一个自增的 _nextID,用来为每个任务生成唯一标识。

2. 消息匹配器 createOnmessageHandler 它为一个特定的任务创建 message 事件监听器。

  • 监听器会检查收到的消息中的 id 是否与本次任务的 id 一致,避免不同任务的响应相互干扰。
  • 如果消息中包含 error 字段,则调用 reject 让 Promise 失败;否则用 resolve 返回 result
  • 一旦匹配成功并处理完毕,监听器会立即移除自身,防止内存泄漏。

3. 执行任务 runTask(processor, parameters, transferableObjects) 这是真正向 Worker 发送任务并返回 Promise 的函数:

  • 生成唯一 id
  • 创建一个 Promise,并通过 addEventListener 绑定上面生成的匹配监听器。
  • 调用 postMessage 将 { id, parameters } 以及可选的 transferableObjects 发送给 Worker。
  • Promise 会在 Worker 返回结果后被 resolve/reject,从而将异步回调转换为 await 风格的调用。

4. 调度入口 scheduleTask(processor, parameters, transferableObjects) 它是对 runTask 的一个简单包装,使用 await 等待结果并重新抛出错误,方便后续扩展(如重试、日志等)。

5. 原型方法 TaskProcessor.prototype.scheduleTask 这是使用者直接调用的公开方法:

  • 惰性创建 Worker:首次调用时才会 new Worker(this._workerPath),避免过早消耗资源。
  • 返回一个 Promise,调用方可以通过 .then 或 await 获取结果。

6. 销毁 TaskProcessor.prototype.destroy 调用 worker.terminate() 终止 Worker,并将引用置为 null,以便垃圾回收释放资源。

3.2 Worker 线程部分

这一侧通过 createTaskProcessorWorker 函数将一个常规的异步任务函数包装为符合通信协议的 Worker 消息处理器。

function createTaskProcessorWorker(workerFunction) {
  async function onMessageHandler({ data }) {
    const transferableObjects = [];
    const responseMessage = {
      id: data.id,
      result: undefined,
      error: undefined,
    };

    try {
      const result = await workerFunction(data.parameters, transferableObjects);
      responseMessage.result = result;
    } catch (error) {
      responseMessage.error = error;
    }

    try {
      postMessage(responseMessage, transferableObjects);
    } catch (error) {
      // 回传结果失败时,降级为只发送可序列化的错误信息
      responseMessage.result = undefined;
      responseMessage.error = `postMessage failed with error: ${error.message}`;
      postMessage(responseMessage);
    }
  }

  function onMessageErrorHandler(event) {
    postMessage({
      id: event.data?.id,
      error: `postMessage failed with error: ${JSON.stringify(event)}`,
    });
  }

  self.onmessage = onMessageHandler;
  self.onmessageerror = onMessageErrorHandler;
  return self;
}

1. 消息主处理函数 onMessageHandler 当 Worker 收到主线程发来的 { id, parameters } 时:

  • 准备 transferableObjects 数组(由任务函数填充,用于传回可转移对象)。
  • 用 try/catch 调用用户提供的 workerFunction(parameters, transferableObjects),这是一个异步函数。
  • 成功时,将返回值赋给 responseMessage.result;失败时,将错误对象赋给 responseMessage.error
  • 然后通过 postMessage(responseMessage, transferableObjects) 将结果及可转移对象发回主线程。
  • 如果“发回结果”这一步本身失败(例如返回的数据不可结构化克隆),则捕获该错误,清空 result,并通过 error.message 构造一个可序列化的错误字符串再次尝试发送,提高鲁棒性。

2. 消息错误处理 onMessageErrorHandler 当主线程发送的消息无法被反序列化时(如包含不可转移的对象),会触发 messageerror 事件。此处处理函数会尝试通过 postMessage 回传一个包含 id 和错误信息的响应,避免主线程无限等待。

3. 挂载与暴露 将 onMessageHandler 和 onMessageErrorHandler 分别绑定到 self.onmessage 和 self.onmessageerror,最后返回 self。这样,Worker 加载该脚本后即可自动监听任务并响应。

3.3 整体设计优点

  • Promise 化通信:主线程得到的是一个标准 Promise,可以用 async/await 编写顺序逻辑,彻底告别回调嵌套。
  • 请求-响应匹配:通过 id 唯一标识每个任务,支持并发调度多个任务而不会错乱。
  • 可转移对象支持:直接传递 transferableObjects,高效转移二进制数据所有权,避免深拷贝开销。
  • 错误隔离与降级:Worker 侧不仅捕获任务执行错误,还处理了“回传结果失败”的极端情况,避免 Worker 静默挂起。
  • 惰性初始化:Worker 只在首次调度时创建,符合按需使用的原则。
  • 可扩展的并发控制:当前实现未内置并发限制,但设计上已预留扩展点。你可以在 scheduleTask 中加入活跃任务计数,当超出最大并发数时缓存任务或返回 undefined,单 Worker 实例即可安全地进行限流。

3.4 使用场景示例

为了让上述封装跑起来,你需要准备一个 Worker 脚本文件(例如 worker.js),内容包含 3.2 节的 createTaskProcessorWorker 函数定义以及对它的调用。你可以直接把 3.2 节代码放在这个文件中,然后在末尾调用它,传入真正的任务函数:

// worker.js
// 将 createTaskProcessorWorker 的定义复制到这里(如上一节所示),
// 或者通过 importScripts 引入一个包含该函数的库文件。

createTaskProcessorWorker(async (params, transferList) => {
  // 执行你自己的耗时计算
  const result = heavyCompute(params); // 假设 heavyCompute 已在 Worker 中定义或引入
  return result;
});

如果使用 ES 模块模式的 Worker(即 options.type = "module"),则可以将 createTaskProcessorWorker 放在一个独立的模块中,然后使用标准的 import 引入。

主线程代码则非常简洁:

const processor = new TaskProcessor('worker.js');
const promise = processor.scheduleTask(someData);
if (promise !== undefined) {
  const res = await promise;
  console.log('计算结果:', res);
}

这种封装将底层的消息传递完全隐藏,开发者只需关心任务逻辑本身,极大简化了 Worker 的使用。

四、总结

Web Worker 是浏览器为 JavaScript 提供的真正的多线程能力,它运行在操作系统级的独立线程上,拥有自己的事件循环和内存空间,通过安全的消息传递与主线程通信。利用它,我们可以将耗时的计算任务平稳地迁移到后台,保持网页的流畅与响应。

将 Worker 的异步消息封装为 Promise 更是点睛之笔。通过任务 ID 和临时监听器的设计,我们既能获得线性的 async/await 代码风格,又能自然地传播错误,还能方便地实现并发控制(Promise.allPromise.race 等)。当你下一次面对复杂的前端计算场景时,不妨尝试为它量身打造一套 Worker + Promise 的解决方案,让 JavaScript 真正发挥多核 CPU 的威力。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐