Skip to content

web worker

1. web worker是什么?

我们知道,js 是单线程的语言。当我们有一些密集型的任务要处理的时候(比如视频解码等),U`I 线程就会被阻塞,甚至浏览器直接卡死,因为主线程被占用,这时候我们希望JS能以多线程的方式去运行。现在前端遇到大量计算的场景越来越多,为了有更好的体验,HTML5 中提出了 Web Worker 的概念。

Web Worker 可以使脚本运行在新的线程中,它们独立于主线程,可以进行大量的计算活动,而不会影响主线程的 UI 渲染。当计算结束之后,它们可以把结果发送给主线程,从而形成了高效、良好的用户体验。

Web Worker 是一个统称,具体可以细分为普通的 Worker、SharedWorker 和 ServiceWorker 等,接下来我们一一介绍其使用方法和适合的场景。

web worker是浏览器的功能,实际上和js语言本身几乎没什么关系,也就是说,JS当前并没有任何支持多线程执行的功能

2. 特点

web worker是独立于主线程的一个线程,当然它为了不阻塞主线程,也有一些限制,比如不能访问DOM,也不能访问其他脚本创建的变量。

因为有上面的限制,所以Web Workers不想多线程编程语言一样,有锁的概念,也不会有线程安全的问题。

简单理解就是,worker 之间以及他们和主程序之间,不会共享任何作用域或资源,他们之间是通过一个基本的事件消息机制相互联系,因此并不会将多线程编程的噩梦带到前端领域。

在Worker内部是无法访问主程序的任何资源的, 也就是说不能访问全局变量,也不能访问页面的DOM或者其他资源,==记住,这是一个完全独立的线程==

但是worker内部可以执行网络操作, (Ajax, webSockets)以及设定定时器,还有,worker可以访问几个重要的全局变量和本地复制,包括navigatorlocationJSONapplicationCache

Web Worker使用场景

  • 处理密集型数学计算
  • 大数据集排序
  • 数据处理(压缩、音频分析、图像处理等)
  • 高流量网络通信

3. web worker的使用

从js主程序(或另一个worker)中,可以这样实例化一个worker:

js
let w1 = new Worker("http://some.url.1/mycoolworker.js");

或者

let w1 = new Worker("./woeker.js");

这个 url应该指向一个js文件的位置(而不是一个html页面),这个文件将被加载到worker中,然后浏览器启动一个独立的线程,让这个文件在这个线程中作为独立的程序运行。

Web Workers是需要运行在服务环境中(http/https协议),也就是如果我们通过本地直接预览html是不行的(file协议),这个时候解决方案有很多,最简单的解决方案是通过编辑器插件来启动一个本地服务预览html。例如,vscode中安装Live Server插件

这种通过url创建的worker称为专用worker(Dedicated Worker)。

1. web worker初体验

它的使用方式非常简单,只需要创建一个Worker对象,然后调用它的postMessage方法,就可以在后台运行一个脚本了。

一个简单的例子: main.js

js
// main.js
// 创建一个 Worker 对象
const worker = new Worker('./worker.js');

// 调用 postMessage 方法,传递一个消息
worker.postMessage('Hello World!');

worker.js

js
// worker.js
// 监听消息
self.addEventListener('message', (event) => {
    console.log(event.data); // 'Hello World!
});

在上面的例子中,我们在main.js中创建了一个Worker对象,然后调用它的postMessage方法,传递了一个消息。 在worker.js中,我们监听了message事件,当main.js中的Worker对象调用postMessage方法时,就会触发message事件,我们就可以在事件回调中获取到传递过来的消息。

注意:worker.js中的self指向的是WorkerGlobalScope对象,它是Worker对象的全局作用域,它的addEventListener方法用来监听事件。

2. 传递数据

1. 基本的数据传递

上面的示例中,我们只是传递了一个字符串,但是实际上,我们可以传递任何数据类型,比如ArrayBufferBlobMessagePort等。

例如: main.js

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

// 创建一个 ArrayBuffer 对象
const buffer = new ArrayBuffer(16);

// 创建一个 Int32Array 对象
const int32View = new Int32Array(buffer);

// 设置 Int32Array 对象的值
for (let i = 0; i < int32View.length; i++) {
    int32View[i] = i * 2;
}

// 传递一个 ArrayBuffer 对象
worker.postMessage(buffer);
// 传递一个 Int32Array 对象
worker.postMessage(int32View);

worker.js

js
// worker.js
self.addEventListener('message', (event) => {
    // 获取 ArrayBuffer 对象
    const buffer = event.data;

    // 创建一个 Int32Array 对象
    const int32View = new Int32Array(buffer);

    // 打印 Int32Array 对象的值
    for (let i = 0; i < int32View.length; i++) {
        console.log(int32View[i]);
    }
});

在上面的例子中,我们在main.js中创建了一个ArrayBuffer对象,然后创建了一个Int32Array对象,最后把这两个对象都传递给了Worker对象。

在worker.js中,我们监听了message事件,然后获取到了传递过来的对象,然后创建了一个Int32Array对象,最后打印了这个对象的值。这里有一个问题就是我们如何知道传递过来的是ArrayBuffer对象还是Int32Array对象呢?

这里有很多种方法可以判断,比如我们可以在传递的时候,把对象的类型也传递过去,或者我们可以在传递的时候,把对象的类型作为key,对象作为value,然后在worker.js中,通过key来获取到对象。

这里我只是引出一个问题,就是web worker中,我们只有一个message事件,同时我们可以传递任何JavaScript对象,所以我们可以根据自己的需求,来定义传递的数据格式。

例如可以定义一个对象,然后把对象的类型作为key,对象作为value,然后在worker.js中,通过key来获取到对象。

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

// 创建一个 ArrayBuffer 对象
const buffer = new ArrayBuffer(16);

// 创建一个 Int32Array 对象
const int32View = new Int32Array(buffer);

// 传递一个 ArrayBuffer 对象
worker.postMessage({
    type: 'ArrayBuffer',
    data: buffer
});

// 传递一个 Int32Array 对象
worker.postMessage({
    type: 'Int32Array',
    data: int32View
});

这里就说这么多了,接下来我们来看一下web worker是怎么把数据传递给主线程的。

2. 加载额外的js脚本

在worker里面,我们可以通过importScripts(..)向Worker加载额外的js脚本:

js
// 在worker内部
impirtScripts('foo.js', 'bar,js')

这些脚本加载时同步的,所以importScripts的调用会阻塞后面的worker代码的执行

3. 传递数据给主线程

主线程通过postMessage方法向worker传递数据,worker也是通过postMessage方法向主线程传递数据。 不同的是主线程通过onmessage属性来监听worker传递过来的数据(也可以通过addEventListener方法来监听),而worker通过addEventListener方法来监听主线程传递过来的数据。

可以通过postMessage方法来向主线程传递数据,这个方法的参数可以是任何JavaScript对象,比如StringNumberBooleanArrayObject等。

worker中同样也有postMessage方法,用于向主线程传递数据。

js
// worker.js
self.addEventListener('message', (event) => {
    // 向主线程传递数据
    self.postMessage('收到了!!!');
});

在主线程中,我们可以通过Worker对象的onmessage属性来监听worker传递过来的数据。

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

worker.onmessage = (event) => {
    console.log(event.data);  // 收到了!!!
};

// 或者
worker.addEventListener('message', function(event) {
	console.log(event.data) // 收到了!!!
})

4. 异常处理

error事件

在web worker中,如果遇到了异常,它是不会抛出异常的,而是会触发error事件。

也不是不会抛出异常,而是抛出的异常不是在主线程中,所以对于主线程来说是无感的,但是我们需要知道这个异常,于是就有了error事件。

js
// worker.js
self.addEventListener('message', (event) => {
    // 抛出异常
    throw new Error('出错了!!!');
});

// worker中监听worker抛出的异常
self.addEventListener('error', (event) => {
    console.log(event.message);
});

在主线程中,我们可以通过Worker对象的onerror属性来监听worker抛出的异常。

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

// 主线程中监听worker抛出的异常
worker.onerror = (event) => {
    console.log(event.message);
};

messageerror事件

除了上面的message事件和error事件之外,web worker还有一个messageerror事件,同样它也同时存在于主线程和worker中。

它的作用是当传递的数据无法被序列化,那么就会触发messageerror事件。

注意了,它和error事件不一样,error事件是当worker抛出异常时触发的,而messageerror事件是当传递的数据无法被序列化时触发的。

worker.js

js
// worker.js
self.addEventListener('message', (event) => {
    // 向主线程传递数据
    self.postMessage('收到了!!!');
});

self.addEventListener('messageerror', (event) => {
    console.log(event.message);
});

main.js

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

worker.postMessage({
    func: () => {
    }
})

worker.onmessageerror = (event) => {
    console.log(event.message);
};

上面只会触发主线程的messageerror事件,但是不会触发error事件。

worker中的messageerror事件和主线程中的messageerror事件也是同理,worker如果传递了无法被序列化的数据,那么就会触发workermessageerror事件。

5. 关闭worker

关闭web worker指的是关闭worker线程,就简简单单的停止worker线程的运行,让worker线程不会有任何反应机会。

关闭了的worker是无法再次启动的,如果想要再次启动,那么就需要重新创建一个worker,没有起死回生的机会。

web worker中,我们可以通过close方法来关闭worker

js
// worker.js
self.addEventListener('message', (event) => {
    // 关闭worker
    self.close();
});

在主线程中,我们可以通过Worker对象的terminate方法来关闭worker

js
// main.js
const worker = new Worker('worker.js');
// 关闭worker
worker.terminate();

6. 总结

总体来说web worker还是比较简单的,上面介绍Worker对象:Worker

对象,只有一个构造函数,两个方法,三个监听事件:

一个构造函数

  • Worker()用来创建一个worker对象

两个方法

  • postMessage():用来向worker发送消息
  • terminate():用来终止worker线程

三个监听事件

  • onmessage:用来监听worker发送的消息
  • onerror:用来监听worker线程的错误
  • onmessageerror:用来监听worker发送的消息的错误

Worker对象文件中,自带一个slef对象,可以用来监听主线程发送的消息,也可以用来向主线程发送消息:

  • self.addEventListener([eventName], (event) => {}):用来监听主线程发送的消息

    • eventName:监听的事件名称
    • message:用来监听主线程发送的消息
    • error:用来监听主线程发送的错误
    • messageerror:用来监听主线程发送的消息的错误
    • event:事件对象
    • data:主线程发送的数据
  • self.postMessage():用来向主线程发送消息

  • self.close():用来关闭worker线程

4. 共享worker

一个共享 worker 可以被多个脚本使用,即使这些脚本正在被不同的 windowiframe 或者 worker 访问。 ** 注意: 共享woker目前只有Firefox和chrome支持。**

1. 创建共享Worker

js
let w1 = new shareWorker("xxx.js")

由于共享woker可以与站点的多个程序实例或者多个页面连接,所以这个Worker需要某种方式来得知消息来自于那个程序。这个唯一的标志称为端口(port),类似于网络连接的端口,所以低矮用程序必须使用worker的port对象用于通信。

js
w1.port.addEventListener("message", handleMessages)

// ...

w1.port.postMessage("something coll...")

还有, 端口连接必须初始化,,方式如下:

js
w1.port.start()

在共享worker内部,必须要处理一个额外的事件:connect,这个事件为这个特定的连接提供了端口对象。

js
let w1 = new SharedWorker('./worker.js')
// 在共享worker内部
w1.addEventListener('connect', function(evt) {
  // 这个连接分配的端口
  let port = evt.ports[0];

  port.addEventListener('message', function(event) {
    // ...
    port.postMessage('...');
    // ...
  })

  // 初始化端口程序
  port.start()
})

除了这个区别以外,共享worker和专用worker在功能上和语义方面都是一样的

5. 共享worker与专用Worker的区别

若是有某个端口连接终止而其他端口连接仍然活跃,那么共享Worker不会终止,而对于专用Worker来说,只要到实例化它的程序的连接终止,他就会终止