一句话阐述 #
Web Worker 是 HTML5 提供的一个可突破 JavaScript 单线程限制的功能。其允许开发者创建线程, 并提供了与主线程之间的通信方法。
概述 #
我们都知道 JavaScript 是单线程的。但是呢,浏览器是多线程(且多进程)的,并且 JavaScript 又支持异步和回调,似乎多线程在 JavaScript 中并非十分必要的需求。然而当前端页面需要做一些计算量大的工作(这些工作往往是背景工作),将其和负责响应 UI 的 JavaScript 主线程放在一起是显然不明智。其势必会拖慢主线程的执行,导致页面响应迟钝,影响用户体验。
Web workers 便在这种情境下产生。它允许开发者创建背景线程,然而这个线程与主线程不在同一上下文环境,他们的变量是不共享的,要实现两个线程间的通信和协作,则需使用 JavaScript 提供的消息通信方法。
Web workers 可分为 dedicated workers 和 shared workers。Dedicated workers 只能与创建它的脚本通信,shared workers 则可和多个脚本通信。
Dedicated workers #
我们先讨论 dedicated workers,由之可阐述 web worker 的绝大多数概念,而 shared workers 只是与 dedicated workers 有稍稍的不同。
如前所述,dedicated workers 只能与创建它的脚本通信。
创建 #
Web worker 的创建非常简单:
// main.js
var myWorker = new Worker('worker.js');如上代码中,我们在 main.js 脚本中创建了一个 dedicated worker,传入了 worker.js 作为其构造参数。前面说过,dedicated worker 只能与创建它的脚本通信。即,在本例中,我们只能在 main.js 中编写与 myWorker 通信的代码。
通信 #
JavaScript 提供了 postMessage 和 onmessage 方法来实现主线程与 worker 线程的通信。双方均用 postMessage 来发送消息,均用 onmessage 来接收消息(没搞明白为啥这两者的命名规则没统一)。因而主线程的 postMessage 和 worker 线程的 onmessage 是相对的,worker 线程的 postMessage 和主线程的 onmessage 是相对的。(这里有点像单片机的双机通信,接对发,发对接。)
// main.js
myWorker.postMessage([firstNumber, secondNumber]);
myWorker.onmessage = function(e) {
console.log(e.data);
}
// worker.js
onmessage = function(e) {
const sumValue = e.data[0] + e.data[1];
postMessage(sumValue);
}在本例中,主线程给 worker 线程发送了一个数组,并且定义了 onmessage 方法用于打印 worker 线程的返回的消息。worker 线程在收到主线程的消息后,将取出数组中的两个值并相加,之后将两数之和发回给主线程。这样我们便通过 worker 线程完成了两数相加的运算。(当然此处是大才小用,但是密集计算需求中,woker 线程非常必要。)
值得注意的是,主线程和 worker 线程中传递数据的形式是消息通信,那么双方进行的是值传递,即使咱们在 postMessage 中传入的是引用类型,比如数组、对象,其传递的也是值,而非引用。实际上,引用数据在消息通信的过程中经历了序列化和反序列化的操作,即,引用数据在被传递出去时,会被序列化成串行数据,接收方接收后,再将其反序列化,构建成为一个拥有相同值的新变量。因而我们在 worker 线程中对变量做的任何操作,是不会在主线程中产生影响的。这很好理解,本身 worker 线程和主线程就不在同一上下文环境中,不然也不至于需要消息通信了。
上下文环境 #
前面说过,worker 线程和主线程不在同一上下文环境中。在主线程中我们经常获取的 window 对象,在 worker 线程中直接使用是会报错的,你也不能直接在 worker 线程中操作 DOM 元素。但是,window 下的很多元素仍然可以使用,比如 WebSocket、IndexedDB 等。更多关于 web worker 中可使用的方法和类,可以点此查看。
在 worker 脚本中 import 其他脚本 #
在 worker 脚本中通过 importScripts 方法可以 import 其他脚本。
importScripts(); /* imports nothing */
importScripts('foo.js'); /* imports just "foo.js" */
importScripts('foo.js', 'bar.js'); /* imports two scripts */
importScripts('//example.com/hello.js'); /* You can import scripts from other origins */ 值得一提的题外话,JavaScript 中 import 的脚本,其加载(下载)完成的顺序是不定的,但是他们会严格按照 import 的顺序被执行。
错误处理 #
worker 线程执行出错时,主线程中的 myWorker.onerror 方法将被触发。(如果不想将错误抛给主线程,也可以通过 preventDefault 方法来阻止。)
在 worker 线程中继续创建子线程 #
worker 线程中可以和主线程一样继续创建子线程。
待确认:worker 线程中创建的 dedicated 子线程是否只能与父级 worker 线程通信,或是也可以与主线程通信?
销毁 #
worker 线程的工作结束后,应当立即将其关闭,以节约系统资源。
*/*/ 主线程
worker.terminate();
// Worker 线程
self.close();Shared Workers #
不同于 dedicated workers,shared workers 可以与多个脚本进行通信,即使这些脚本属于不同的 window、iframe,甚至是 worker。
创建 #
Share workers 的创建与 dedicated workers 没有什么区别,只是构造方法的不同。
var myWorker = new SharedWorker('worker.js');通信 #
由于要与不同的脚本通信,shared workers 在此处比 dedicated workers 要稍显复杂。无论是发送还是接收,都需要通过 port 对象来引用对应方法。我们在每次使用 new 方法创建一个 SharedWorker 对象时,其实都会分配一个 port,通过 port 可以区分不同脚本之间的通信信息。
假设咱们的 worker.js 中有一个实现两数相乘的方法。我们的主线程中有两个脚本,multiple.js 和 square.js 分别需要通过 worker 线程实现乘法和平方(传入两个相同的乘数)。
// multiple.js
var myWorker = new SharedWorker("worker.js");
myWorker.port.postMessage([firstNumber, secondNumber]);
myWorker.port.onmessage = function(e) {
console.log('multiple: ', e.data);
}
// square.js
var myWorker = new SharedWorker("worker.js");
myWorker.port.postMessage([squareNumber, squareNumber]);
myWorker.port.onmessage = function(e) {
console.log('square: ', e.data);
}
// worker.js
onconnect = function(e) {
var port = e.ports[0];
port.onmessage = function(e) {
const workerResult = (e.data[0] * e.data[1]);
port.postMessage(workerResult);
}
}线程安全 #
Worker 接口最终产生的是操作系统层面的真实线程。那么,有没有可能会引发线程安全问题呢?
值得庆幸,根据 mozilla.org 的描述:几乎很难引发并发问题。因为 web workers 从一开始就是基于线程安全设计的,其整个运行过程不会使用到非线程安全的组件或者 DOM 元素。而 web worker 线程在运行时也像是一个仅有序列化数据输入输出的孤岛。因而「你必须在代码中费尽心思,才有可能成功引发线程安全问题」。
在 umi 中的使用 #
前面都是从 JavaScript 的角度讲如何使用 web workers。在实际项目中,我们可能会用到各种开发框架,相对与手撸一个项目,封装了各种功能和配置的框架会让开发的效率更高。然而,得其利必然受其困,很多框架的配置并非那么的自由。在此以我常用的 umi 框架为例,介绍一下在 web workers 在其中的使用。
当然,在 umi 中使用 web workers 和在纯 JavaScript 中是没有本质区别的。关键就在于如何在 umi 中配置 webpeck。
Umi 的配置文件为 .umirc.ts,其中提供了 webpack-chain 配置项对 webpack 进行配置。
chainWebpack: function(memo, { webpack }) {
memo.module
.rule('compile')
.test('/.worker.js$/')
.use('worker')
.loader('worker-loader');
memo.toString();
},根据配置, .worker.js 后缀的文件将被识别为 web worker 脚本,并使用 worker-loader 对其进行编译。
配置好后,我们就可以在 umi 中正常使用 web workers 了。
结尾 #
至此,web workers 的基本概念和使用就介绍完了。个人认为 web workers 这个东西,是为特殊需求而生的,有点违背了 JavaScript 当初设计的思想,当然,web workers 中的各种机制保障了线程安全并且 UI 主线程不会受影响,但这个东西还是给我一种畸形产物的感觉。所以,个人觉得如果你不知道 web workers 是什么东西,那你没有必要刻意去用它,如果你的页面响应出现卡顿了,那么先从其他方面找找问题,看看有没有不规范的,能优化的地方。