跳转到主要内容
最佳的Web Worker教程
阅读需 11 分钟

Build the internal tools you need(Dashboard, Workflow Management, CRM, Support and Billing management...) in minutes.

最佳的Web Worker教程

Web Worker是一个可以在后台运行JavaScript的JavaScript API。它旨在允许执行复杂的计算而不会阻塞主线程。这对Web开发人员非常有用,但并没有广泛使用。本文将介绍如何使用Web Worker。

什么是Web Worker

众所周知,JavaScript语言的一个主要特点是其单线程性质,一次只能同步处理一个任务。这也是针对Node.js的后端开发者的主要批评点之一。

然而,JavaScript最初是有意设计为单线程语言,以适应当时的用途。

JavaScript的最初目标是通过操作DOM或BOM元素来促进Web页面与用户的交互。为了在这个上下文中追求效率,必须指定只有一个线程在任何时间点可以直接操作页面元素,以避免资源竞争和数据同步等问题,以确保系统的稳定性和安全性。

然而,JavaScript并不限制于线性任务处理。JavaScript具有消息队列和事件循环机制,通过异步消息处理实现并发处理。在高I/O并发事务处理中,不需要手动创建和销毁线程,也不需要额外的线程管理空间,因此性能优越。结果,探索在服务器端使用JavaScript的Node.js在高并发网络请求处理方面表现出色。

虽然JavaScript通过其异步机制有效地解决了与高I/O相关的性能问题,但其单线程执行的基本特性仍然存在。这是因为在处理CPU密集型任务时无法充分利用其计算资源,这是因为它不能充分利用现代多核多线程机器的计算资源。

在现代大型前端项目中,随着代码复杂性的增加,本地计算密集型任务也变得需要。如果将JavaScript项目在单个线程中运行,应用程序可能会变得繁忙,忽略了频繁的用户交互,用户体验可能不佳。更严重的是,如果有太多的计算密集型任务,可能会导致资源饱和,使网页不再响应。因此,在Web项目中需要本地多线程计算能力,这就是Web Worker诞生的原因。

Web Worker作为HTML5的标准引入,官方定义如下:

Web Worker允许在与Web应用程序的主执行线程分开的后台线程中运行脚本操作。

这样,JavaScript脚本可以创建多个线程,充分利用CPU的多核计算能力,而不会阻塞主线程(通常是UI渲染线程)。

尽管Web Worker是HTML5的一部分,但实际上它在2009年提出了W3C的草案。因此,它具有很好的兼容性,并且在大多数主要的Web浏览器中得到支持。

web_worker

Web Worker的限制

Web Worker基本上不会破坏JavaScript的单线程性质。

实际上,Web Worker脚本内的代码不能直接操作DOM节点,也不能使用大多数BOM(浏览器对象模型)API。它的全局环境不是Window,而是DedicatedWorkerGlobalScope。Worker在沙箱中运行,执行与主线程完全独立的JavaScript文件。

这些限制是为了避免在前文提到的资源竞争问题中使用Worker。主要用途是作为主线程的辅助,处理高CPU密集型数据处理任务,并通过线程间通信将执行结果返回给主线程。通过这个过程,主线程可以继续响应用户交互,有效地防止了页面延迟的问题。

使用Web Worker

目前,Web Worker在浏览器中得到了广泛支持,只需提供Worker脚本的URI并进行实例化即可使用。

/* main.js */

const worker = new Worker("./worker.js")

通信

Worker和主线程之间的通信只需要两个API,即用于接收消息的onmessageaddEventListener以及用于发送消息的postMessage,可以无缝地实现基于消息的交互。

/* main.js */
const worker = new Worker("./worker.js");

// 从主线程发送消息
worker.postMessage({ data: '从主线程发送的数据' });

// 主线程接收消息
worker.onmessage = (e) => {
const { data } = e;
if (!data) return;
console.log(data);
}
/* worker.js */
// Worker线程接收消息
self.addEventListener('message', (e) => {
const { data } = e;
if (!data) return;
// Worker线程发送消息
self.postMessage({data: 'Worker收到了数据'})
});

注意:在Worker内部,可以使用this.xxself.xx以及直接使用xx,它们都处于相同的作用域,引用全局变量DedicatedWorkerGlobalScope,可以互换使用。

终止

有两种方法可以终止Worker。可以在内部终止,也可以从主线程发出终止指令。



/* main.js */
worker.terminate();
/* worker.js */
self.close();

高级用法:基于Promise的通信

基于前面的部分,已经可以使用Worker的API相对轻松地利用浏览器的多线程功能。但是,缺少一些常用功能,如异步响应等,这在工程应用程序中通常是需要的。接下来,将介绍如何实现这些功能。

首先,需要一个异步回调集合,称为actionHandlerMap。它用于存储等待Worker响应的Promise解析方法,并可以使用唯一的标识符(确保唯一性)指定键。接下来,需要封装原生的postMessageonmessage方法。

在发送消息时,需要在消息中包含一个id,并在actionHandlerMap中存储当前Promise的解析方法以等待Worker的响应。

onmessage监听器方面,接收到Worker的响应后,将其与相应的Promise匹配并执行.then()方法。完成后,从集合中删除Promise的解析函数。

/* main.js */
let fakeId = 0;
class MainThreadController {
constructor(options) {
this.worker = new Worker(options.workerUrl, { name: options.workerName });

// 用于等待异步回调的集合
this.actionHandlerMap = {};

this.worker.onmessage = this.onmessage.bind(this);
}

onmessage(e) {
const { id, response } = e.data;
if (!this.actionHandlerMap[id]) return;

// 执行相应的Promise解析
this.actionHandlerMap[id].call(this, response);
delete this.actionHandlerMap[id];
}

postMessage(action) {
// 在实际使用中,可以使用业务ID作为键或生成键
const id = fakeId++;
return new Promise((resolve, reject) => {
const message = {
id,
...action,
};
this.worker.postMessage(message);
this.actionHandlerMap[id] = (response) => {
resolve(response);
};
});
}
}

const mainThreadController = new MainThreadController({ workerUrl: './worker.js', workerName: 'test-worker' });
mainThreadController
.postMessage({
actionType: 'asyncCalc',
payload: { msg: '向Worker发送消息', params: 1 },
})
.then((response) => console.log('从Worker接收到消息:', response.msg));

Worker部分的处理要简单得多。当计算完成后,将请求的ID包含在响应中。

/* worker.js */
class WorkerThreadController {
constructor() {
this.worker = self;

// 用于等待异步回调的集合
this.actionHandlerMap = {};

this.worker.onmessage = this.onmessage.bind(this);
}

async onmessage(e) {
const { id, actionType, payload } = e.data;
switch (actionType) {
case 'print':
console.log(payload.msg);
self.postMessage({ id, response: { msg: '消息已打印。' } });
break;

case 'asyncCalc':
// 模拟异步处理情景
const result = await new Promise((resolve) => setTimeout(() => resolve(payload.params * 2), 1000));
self.postMessage({ id, response: { msg: `计算的结果为 ${result}` } });
break;

default:
break;
}
}
}

const workerThreadController = new WorkerThreadController();

当然,在Worker端可以进一步改进。如果Worker需要处理多种类型的计算,onmessage函数中的switch语句可能会变得很长。基于字符串的检查也可能不够可靠。通过使用策略模式将Worker内部逻辑封装起来,可以简化Worker内部的逻辑。

/* worker.js */
// 这可以单独提取到另一个文件并导入
const api = {
print(payload) {
console.log(payload.msg);
return { msg: '消息已打印。' };
},
async asyncCalc(payload) {
const result = await new Promise((resolve) => setTimeout(() => resolve(payload.params * 2), 1000));
return { msg: `计算的结果为 ${result}` };
},
};

class WorkerThreadController {
constructor() {
this.worker = self;

// 用于等待异步回调的集合
this.actionHandlerMap = {};

this.worker.onmessage = this.onmessage.bind(this);
}

async onmessage(e) {
const { id, actionType, payload } = e.data;
const result = await api[actionType].call(this, payload);
self.postMessage({ id, response: result });
}
}

const workerThreadController = new WorkerThreadController();

因此,建立了一个简单且方便的基于Promise的Worker。

结论

总结一下,本文简要介绍了Web Worker的概念、其功能和限制,并为读者提供了全面的理解以及如何使用Web Worker的用例。还提出了一种封装原生Worker API以实现基于Promise调用的解决方案,并最终建议使用在团队中已经广泛使用且成熟的解决方案。希望这对前端开发人员对未来的Worker扩展感兴趣时有所帮助。

Related Articles

2024年在Mac上优雅使用nvm管理Node.js

Node.js作为前端能力的基础,不再仅仅是“JS服务器运行时”。

构建内部工具的最佳开源低代码平台

开源工具很常见,但很少见到有1万颗星的。我要分享一个在海外获得1万颗星的产品。有很多低代码工具,但真正有用的很少。这个工具主要被北美的开发人员使用,他们的Discord社区非常活跃。

低代码与传统开发:选择中的最优效率

随着开发实践的持续演进,低代码工具的出现为各种开发场景带来了创新解决方案。这减轻了从头开始项目的需要,为更重要的业务活动节省了大量时间。然而,重要的是要认识到,虽然低代码加速了开发,但这是以牺牲一定的灵活性为代价的。因此,并非所有的解决方案都适合低代码开发。让我们更深入地分析哪些场景更适合使用低代码工具,以及哪些场景中传统代码开发更为合适。在这一探索过程中,低代码与传统开发的关键比较始终是最重要的,强调选择最有效方法的重要性。