都知道,js是单线程的。但是随着js的责任和能力越来越大,js往往需要大量计算,并会被计算长时间阻塞,从而会造成页面卡顿,影响用户体验,为了解决这个弊端,web worker产生了。
定义
web worker是h5标准的一部分,这一规范定义了一套API,允许js主线程之外开辟新的worker线程,并将一段js脚本运行其中,赋予开发者利用js操作多线程的能力。 worker和js主线程能同时运行,互不阻塞。所以,在我们有大量运算任务时,可以把运算任务交给work线程处理,worker线程计算完成后,再返回结果给js主线程。这样,js主线程只用专注处理业务逻辑,不用耗费过多时间去处理大量复杂计算,从而减少阻塞时间,也提高了运算效率。
作用
虽然worker线程实在浏览器环境中被唤起,但是它与当前页面窗口运行在不同的全局上下文,我们常用的window和parent对象在work中不可用。另外在worker上下文中,也不能操作dom,document对象也不存在。但是location和navigator对象可读可访问,除此之外,绝大多数window上的方法和属性,都被共享到worker的上下文全局对象WorkerGlobalScope中。同样,worker线程也有一个顶级对象self。
使用
const worker = new Worker(path, option);
先看下参数说明
| 参数 | 说明 |
|---|---|
| path | 有效的js脚本的地址,必须遵守同源策略。无效的js地址或者违反同源策略,会抛出SECURITY_ERR 类型错误 |
| options.type | 可选,用以指定 worker 类型。该值可以是 classic 或 module。 如未指定,将使用默认值 classic |
| options.credentials | 可选,用以指定 worker 凭证。该值可以是 omit, same-origin,或 include。如果未指定,或者 type 是 classic,将使用默认值 omit (不要求凭证) |
| options.name | 可选,在 DedicatedWorkerGlobalScope的情况下,用来表示 worker 的 scope 的一个 DOMString值,主要用于调试目的。 |
主线程和worker通信
// 这里是主线程
const worker = new Worker('/worker.js');
worker.addEventListener('message', e=>{ // 接受消息
console.log(e.data) // worker线程发送的消息,小弟来了(下面写了)
})
worker.postMessage('我tm来了'); // 发送消息给worker线程
// worker线程
self.addEventListener('message',e=>{
console.log(e.data) // 我tm来了,就是上面的主线程发送的消息
self.postMessage('小弟来了')// 向主线程发送消息
})
postMessage方法接受的参数可以是字符串,对象,数组等。 主线程和worker线程之间的数据传递是传值而不是传地址。也就是说,都是深拷贝的。
监听错误信息
web worker提供两个事件监听错误,error和messageerror。下面是区别
| 事件 | 描述 |
|---|---|
| error | 当worker内部出现错误时触发 |
| messageerror | 当 message 事件接收到无法被反序列化的参数时触发 |
| 监听方式和接受消息一致: |
// 主线程
const worker = new Worker('/worker.js');
worker.addEventListener('error',err={ //messageerror也一样
console.log(err.message)
})
// worker线程
self.addEventListener('error',err={ //messageerror也一样
console.log(err.message)
})
关闭worker线程
worker线程的关闭在主线程和worker线程中都能操作,但影响不同。
// 主线程
const worker = new Worker('...')
worker.terminate() // 关闭
// worker线程
self.close();
两种关闭方式种,worker线程当前的eventLoop的任务还是会继续执行,**work自主关闭的模式下,**下一个eventLoop则会被直接忽略,不会继续执行。区别在于,主线程的关闭,两线程的连接会立刻停止,即使worker的当前eventLoop中仍有待执行的任务继续调用postMessage通信,主线程也不会再收到了,而worker内部关闭,不会直接断开两线程连接,而是等worker中的eventLoop所有任务(包括通信任务)执行完,再关闭。 下面是主线程关闭work的例子 主线程关闭
// 主线程
const work = new Worker('./worker.js');
work.addEventListener('message', (e) => {
console.log('主线程的监听', e.data);
work.terminate();
});
work.postMessage('主线程发送消息')
// work线程
self.addEventListener('message', (e) => {
console.log('次线程的监听', e.data);
self.postMessage('次线程的消息1');
setTimeout(() => {
console.log('子定时器的消息');
self.postMessage('次线程的消息2');
});
new Promise((resolve) => {
console.log('子Promise的消息33');
self.postMessage('次线程的消息33');
resolve();
}).then(() => {
console.log('子Promise的消息');
self.postMessage('次线程的消息3');
});
for (let i = 0; i < 100; i++) {
if (i === 99) {
console.log('子循环消息');
self.postMessage('次线程的消息4');
}
}
});
// 输出=》次线程的监听 主线程发送消息、子Promise的消息33、子循环消息、子Promise的消息、
// 子定时器的消息、主线程的监听 次线程的消息1
下面是子线程的自主关闭例子 自主关闭
// 主线程
const work = new Worker('./worker.js');
work.addEventListener('message', (e) => {
console.log('主线程的监听', e.data);
// work.terminate();
});
work.postMessage('主线程发送消息')
// 次线程
self.addEventListener('message', (e) => {
console.log('次线程的监听', e.data);
postMessage('次线程的消息1');
close();
setTimeout(() => {
console.log('子定时器的消息');
postMessage('次线程的消息2');
});
new Promise((resolve) => {
console.log('子Promise的消息33');
postMessage('次线程的消息33');
resolve();
}).then(() => {
console.log('子Promise的消息');
postMessage('次线程的消息3');
});
for (let i = 0; i < 100; i++) {
if (i === 99) {
console.log('子循环消息');
postMessage('次线程的消息4');
}
}
});
// 输出:次线程的监听 主线程发送消息、
// 子Promise的消息33
// 子循环消息
// 子Promise的消息
// 主线程的监听 次线程的消息1
// 主线程的监听 次线程的消息33
// 主线程的监听 次线程的消息4
// 主线程的监听 次线程的消息5
// 特别注意:setTimeout中的没执行
特别注意的是定时器的任务和通信都没执行,因为这已经下一轮eventLoop(宏任务)了
worker线程引用其他js文件
使用importScripts方法
esModule模式
书接上面的引用,importScript可能会失败,因为我们引入的js可能是esmodule文件。
// main.js(主线程)
const worker = new Worker('/worker.js', {
type: 'module' // 指定 worker.js 的类型
});
这样就行了。 这时候的work进程也得是esmodule。
主线程和worker线程可传递哪些数据类型
有一些参数不可传递,会导致DATA_CLONE_ERR错误,例如方法
// main.js(主线程)
const myWorker = new Worker('/worker.js'); // 创建worker
const fun = () => {};
myWorker.postMessage(fun); // Error:Failed to execute 'postMessage' on 'Worker': ()=>{} could not be cloned.
postMessage可传递的数据可以是由结构化克隆算法处理的任何值或者js对象。 不能处理的数据是: Error和function Dom节点 对象的某些特定参数不会被保留: reg对象的lastIndex字段不会被保留 属性描述符不会被复制,如read-only复制出来就是read-write了 原型链上的属性也不会被追踪以及复制。 结构化克隆算法支持的数据类型:
| 类型 | 说明 |
|---|---|
| 所有的原始类型 | symbols 除外 |
| Boolean 对象 | |
| String 对象 | |
| Date | |
| RegExp | lastIndex 字段不会被保留。 |
| Blob | |
| File | |
| FileList | |
| ArrayBuffer | |
| ArrayBufferView | 这基本上意味着所有的 类型化数组 ,如 Int32Array 等。 |
| ImageData | |
| Array | |
| Object | 仅包括普通对象(如对象字面量) |
| Map | |
| Set |
shareWorker
是一种特殊的worker,可以被多个浏览上下文访问,比如多个window,iframes和worker,但这些浏览上下文必须同源。他们实现于一个不同于普通worker的接口,具有不同的全局组用于SharedWorkerGlobalScope,但是继承自WorkerGlobalScope
ShareWorker线程的创建和使用跟worker类似,事件和方法也基本一样。不同点在于,主线程与ShareWorker线程是通过messagePort建立起链接,数据通讯方法都挂载在ShareWorker.port上。 值得注意的是,如果采用addEventListenr来接受message事件,那么在主线程初始化ShareWorker后,还要嗲用ShareWorker.port.start()方法来手动开启端口。
// main.js(主线程)
const myWorker = new SharedWorker('./sharedWorker.js');
myWorker.port.start(); // 开启端口
myWorker.port.addEventListener('message', msg => {
console.log(msg.data);
})