主题
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可以访问几个重要的全局变量和本地复制,包括navigator
、location
、JSON
、applicationCache
。
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. 基本的数据传递
上面的示例中,我们只是传递了一个字符串,但是实际上,我们可以传递任何数据类型,比如ArrayBuffer
、Blob
、MessagePort
等。
例如: 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
对象,比如String
、Number
、Boolean
、Array
、Object
等。
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
如果传递了无法被序列化的数据,那么就会触发worker
的messageerror
事件。
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
可以被多个脚本使用,即使这些脚本正在被不同的 window
、iframe
或者 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来说,只要到实例化它的程序的连接终止,他就会终止