如何在Angular中使用Web Worker在不同线程上执行函数
作者:Vlado Tesanovic ✏️
借助Web Worker,我们可以在后台线程中执行脚本,从而让主线程可以专注于 UI 工作。默认情况下,Web Worker 接受文件 URL 作为参数,但在我们的场景中,这并不合适,因为我们主要使用 TypeScript 作为编程语言,并且不希望将其与 JavaScript 混用。第二个问题是脚本需要固定的 URL,而我们使用 Webpack 来打包和合并文件,因此使用未打包的文件并非最佳方案。
Worker类是ServiceWorker和SharedWorker的基础类。SharedWorker 与 Worker 类似,但它可以从多种不同的上下文中访问,包括弹出窗口、iframe 等。ServiceWorker 则完全不同,不在本次演示的讨论范围之内。
工作线程执行的代码与主线程执行的代码运行在不同的上下文中。在工作线程中运行代码时,我们无法操作 DOM 元素、使用 window 对象等等。工作线程运行的上下文称为DedicatedWorkerGlobalScope,其权限和可执行的操作都非常有限。
工作线程的常见用例包括执行繁重处理的纯函数。由于我们不希望它们降低 Web 应用程序的性能,因此应该将它们移到工作线程中。
工作线程可以通过消息传递postMessage方法与主线程通信。通信可以是双向的,这意味着工作线程和主线程可以互相发送消息。
主线程和工作线程都可以互相监听和发送消息。
让我们创建一个InlineWorker类,它接受一个函数作为参数,并在另一个线程中运行该函数,如下所示:
import { Observable, Subject } from 'rxjs';
export class InlineWorker {
private readonly worker: Worker;
private onMessage = new Subject<MessageEvent>();
private onError = new Subject<ErrorEvent>();
constructor(func) {
const WORKER_ENABLED = !!(Worker);
if (WORKER_ENABLED) {
const functionBody = func.toString().replace(/^[^{]*{\s*/, '').replace(/\s*}[^}]*$/, '');
this.worker = new Worker(URL.createObjectURL(
new Blob([ functionBody ], { type: 'text/javascript' })
));
this.worker.onmessage = (data) => {
this.onMessage.next(data);
};
this.worker.onerror = (data) => {
this.onError.next(data);
};
} else {
throw new Error('WebWorker is not enabled');
}
}
postMessage(data) {
this.worker.postMessage(data);
}
onmessage(): Observable<MessageEvent> {
return this.onMessage.asObservable();
}
onerror(): Observable<ErrorEvent> {
return this.onError.asObservable();
}
terminate() {
if (this.worker) {
this.worker.terminate();
}
}
}
上面代码中最重要的部分是一个类,它将一个函数转换为字符串,并创建ObjectURL一个对象,该对象将通过构造函数传递给工作类。
const functionBody = func.toString().replace(/^[^{]*{\s*/, '').replace(/\s*}[^}]*$/, '');
this.worker = new Worker(URL.createObjectURL(
new Blob([ functionBody ], { type: 'text/javascript' })
));
如何使用 InlineWorker 类
假设我们在 Angular 中有一个函数(如上面代码块中显示的类),我们希望在后台处理它。
我们将开发一个应用程序,用于计算范围内有多少个质数。
主线程会将限制参数发送给工作线程,工作线程完成任务后会将结果返回给主线程并终止工作线程。
需要注意的是,我们不能使用在传递给 InlineWorker 的回调函数之外定义的任何方法、变量或函数。
如果需要传递参数(postMessage 函数接受任何参数),则必须使用postMessage方法。
import { Component, OnInit } from '@angular/core';
import { InlineWorker } from './inlineworker.class';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
result = 0;
ngOnInit() {
const worker = new InlineWorker(() => {
// START OF WORKER THREAD CODE
console.log('Start worker thread, wait for postMessage: ');
const calculateCountOfPrimeNumbers = (limit) => {
const isPrime = num => {
for (let i = 2; i < num; i++) {
if (num % i === 0) { return false; }
}
return num > 1;
};
let countPrimeNumbers = 0;
while (limit >= 0) {
if (isPrime(limit)) { countPrimeNumbers += 1; }
limit--;
}
// this is from DedicatedWorkerGlobalScope ( because of that we have postMessage and onmessage methods )
// and it can't see methods of this class
// @ts-ignore
this.postMessage({
primeNumbers: countPrimeNumbers
});
};
// @ts-ignore
this.onmessage = (evt) => {
console.log('Calculation started: ' + new Date());
calculateCountOfPrimeNumbers(evt.data.limit);
};
// END OF WORKER THREAD CODE
});
worker.postMessage({ limit: 300000 });
worker.onmessage().subscribe((data) => {
console.log('Calculation done: ', new Date() + ' ' + data.data);
this.result = data.data.primeNumbers;
worker.terminate();
});
worker.onerror().subscribe((data) => {
console.log(data);
});
}
}
如我们所见,我们将一个匿名函数作为参数传递给了 InlineWorker。传递的函数的上下文是隔离的,这意味着我们无法访问其外部的任何内容。如果我们尝试这样做,它将是 undefined。
我们的应用程序流程大致如下:
我们需要在 postMessage 和方法前面加上 @ts-ignore 注解onmessage,因为 TypeScript 无法读取当前上下文中的定义。在这种情况下,TypeScript 确实帮不上什么忙。
回调函数内部的监听器onmessage将监听传递给此工作线程的任何消息,在本例中,它将calculateCountOfPrimeNumbers使用传递的参数调用该工作线程。
函数会进行计算,并通过postMessage方法将结果传递给主线程上的监听器。
和:
worker.postMessage({ limit: 10000 });
我们将触发一个工作线程的执行。由于本示例是用 Angular 编写的,我们将使用 RXJS 可观察对象来传递和监听数据变化。
下一行,我们将订阅来自工作进程的消息。
worker.onmessage().subscribe((data) => {
console.log(data.data);
worker.terminate();
});
简单来说,我们将结果输出到控制台,然后终止工作线程,使其无法继续使用。我们可以向一个工作线程发送多条消息并接收多个结果,不像上面的示例那样会被锁定以执行单个任务。
订阅可观察对象非常重要,onerror因为这是查看工作线程中发生的错误的唯一方法。
演示时间
这里是使用 worker 实现的演示:https://angular-with-worker-logrocket.surge.sh/(不阻塞 UI)

这是不使用 worker 的演示:https://angular-without-worker-logrocket.surge.sh/(计算运行时 UI 会被阻塞)
结论
在这篇文章中,我们学习了如何将繁重的处理任务从主线程转移到后台线程,而不会阻塞主线程,从而在我们的应用程序中提供良好的用户体验。
Web Workers 是Web API 的一部分,这意味着它们只能在浏览器中使用,值得注意的是,所有主流浏览器都对它们提供了良好的支持。
编者按:发现本文有误?您可以在这里找到正确版本。
插件:LogRocket,一款用于 Web 应用的 DVR

LogRocket是一款前端日志工具,可让您重现问题,如同在您自己的浏览器中发生一样。无需猜测错误原因,也无需用户提供屏幕截图和日志转储,LogRocket 即可让您重现会话,快速了解问题所在。它与任何框架的应用程序完美兼容,并提供插件来记录来自 Redux、Vuex 和 @ngrx/store 的额外上下文信息。
除了记录 Redux 操作和状态之外,LogRocket 还会记录控制台日志、JavaScript 错误、堆栈跟踪、包含标头和正文的网络请求/响应、浏览器元数据以及自定义日志。它还会对 DOM 进行插桩,记录页面上的 HTML 和 CSS,即使是最复杂的单页应用程序,也能生成像素级精确的视频。
免费试用。
文章“如何在 Angular 中使用 web worker 在不同的线程上执行函数”最初发表于LogRocket 博客。
文章来源:https://dev.to/bnevilleoneill/how-to-execute-a-function-with-a-web-worker-on-a- Different-thread-in-angular-akh



