主题
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来说,只要到实例化它的程序的连接终止,他就会终止
