web worker

48 阅读5分钟

都知道,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
RegExplastIndex 字段不会被保留。
Blob
File
FileList
ArrayBuffer
ArrayBufferView这基本上意味着所有的 类型化数组 ,如 Int32Array 等。
ImageData
Array
Object仅包括普通对象(如对象字面量)
Map
Set

shareWorker

是一种特殊的worker,可以被多个浏览上下文访问,比如多个window,iframes和worker,但这些浏览上下文必须同源。他们实现于一个不同于普通worker的接口,具有不同的全局组用于SharedWorkerGlobalScope,但是继承自WorkerGlobalScope image.png

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);
})