【深入吧,HTML 5】 性能 & 集成 —— Web Workers

博客 有更多精品文章哟。

修订

  • 2019-01-16
    • 增加使用 importScripts 跨域时,使用相对路径报错的原因说明。

前言

JavaScript 采用的是单线程模型,也就是说,所有任务都要在一个线程上完成,一次只能执行一个任务。有时,我们需要处理大量的计算逻辑,这是比较耗费时间的,用户界面很有可能会出现假死状态,非常影响用户体验。这时,我们就可以使用 Web Workers 来处理这些计算。

Web Workers 是 HTML5 中定义的规范,它允许 JavaScript 脚本运行在主线程之外的后台线程中。这就为 JavaScript 创造了 多线程 的环境,在主线程,我们可以创建 Worker 线程,并将一些任务分配给它。Worker 线程与主线程同时运行,两者互不干扰。等到 Worker 线程完成任务,就把结果发送给主线程。

Web Workers 与其说创造了多线程环境,不如说是一种回调机制。毕竟 Worker 线程只能用于计算,不能执行更改 DOM 这些操作;它也不能共享内存,没有 线程同步 的概念。

Web Workers 的优点是显而易见的,它可以使主线程能够腾出手来,更好的响应用户的交互操作,而不必被一些计算密集或者高延迟的任务所阻塞。但是,Worker 线程也是比较耗费资源的,因为它一旦创建,就一直运行,不会被用户的操作所中断;所以当任务执行完毕,Worker 线程就应该关闭。

Web Workers API

一个 Worker 线程是由 new 命令调用 Worker() 构造函数创建的;构造函数的参数是:包含执行任务代码的脚本文件,引入脚本文件的 URI 必须遵守 同源策略

Worker 线程与主线程不在同一个全局上下文中,因此会有一些需要注意的地方:

  • 两者不能直接通信,必须通过消息机制来传递数据;并且,数据在这一过程中会被复制,而不是通过 Worker 创建的实例共享。详细介绍可以查阅 worker中数据的接收与发送:详细介绍。
  • 不能使用 DOM、windowparent 这些对象,但是可以使用与主线程全局上下文无关的东西,例如 WebScoketindexedDBnavigator 这些对象,更多能够使用的对象可以查看Web Workers可以使用的函数和类。

工作流程

  1. 在构造函数中传入脚本文件地址进行实例化的过程中,会通过异步的方式来加载这个文件,因此并不会阻塞后续代码的运行。此时,如果脚本文件不存在,Worker 只会 静默失败,并不会抛出异常。
  2. 在主线程向 Worker 线程发送消息时,会通过 中转对象 将消息添加到 Worker 线程对应 WorkerRunLoop 的消息队列中;此时,如果 Worker 线程还未创建,那么消息会先存放在临时消息队列,等待 Worker 线程创建后再转移到 WorkerRunLoop 的消息队列中;否则,直接将消息添加到 WorkerRunLoop 的消息队列中。

Worker 线程向主线程发送的消息也会通过 中转对象 进行传递;因此,总得来讲 Worker 的工作机制就是通过 中转对象 来实现消息的传递,再通过 message 事件来完成消息的处理。

使用方式

Web Workers 规范中定义了两种不同类型的线程:

  • Dedicated Worker(专用线程),它的全局上下文是 DedicatedWorkerGlobalScope 对象,只能在一个页面使用。
  • Shared Worker(共享线程),它的全局上下文是 SharedWorkerGlobalScope 对象,可以被多个页面共享。

专用线程

下面代码最重要的部分在于两个线程之间怎么发送和接收消息,它们都是使用 postMessage 方法发送消息,使用 onmessage 事件进行监听。区别是:在主线程中,onmessage 事件和 postMessage 方法必须挂载在 Worker 的实例上;而在 Worker 线程,Worker 的实例方法本身就是挂载在全局上下文上的。

Demo


<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Web Workers 专用线程title>
head>
<body>
  <input type="text" name="" id="number1">
  <span>+span>
  <input type="text" name="" id="number2">
  <button id="button">确定button>
  <p id="result">p>

  <script src="./main.js">script>
body>
html>
复制代码
// main.js

const number1 = document.querySelector("#number1");
const number2 = document.querySelector("#number2");
const button = document.querySelector("#button");
const result = document.querySelector("#result");

// 1. 指定脚本文件,创建 Worker 的实例
const worker = new Worker("./worker.js");

button.addEventListener("click", () => {
  // 2. 点击按钮,把两个数字发送给 Worker 线程
  worker.postMessage([number1.value, number2.value]);
});

// 5. 监听 Worker 线程返回的消息
// 我们知道事件有两种绑定方式,使用 addEventListener 方法和直接挂载到相应的实例
worker.addEventListener("message", e => {
  result.textContent = e.data;
  console.log("执行完毕");
})
复制代码
// worker.js

// 3. 监听主线程发送过来的消息
onmessage = e => {
  console.log("开始后台任务");
  const result= +e.data[0]+ +e.data[1];
  console.log("计算结束");

  // 4. 返回计算结果到主线程
  postMessage(result);
}
复制代码

共享线程

共享线程虽然可以在多个页面共享,但是必须遵守同源策略,也就是说只能在相同协议、主机和端口号的网页使用。

示例基本上与专用线程的类似,区别是:

  • 创建实例的构造器不同。
  • 主线程与共享线程通信,必须通过一个确切打开的端口对象;在传递消息之前,两者都需要通过 onmessage 事件或者显式调用 start 方法打开端口连接。而在专用线程中这一部分是自动执行的。

端口对象会被上文所讲的 中转对象(WorkerMessagingProxy) 调用,由 中转对象 来决定哪个发送者对应哪个接收者,具体的流程可以看 Web Worker在WebKit中的实现机制。

Demo

// main.js

const number1 = document.querySelector("#number1");
const number2 = document.querySelector("#number2");
const button = document.querySelector("#button");
const result = document.querySelector("#result");

// 1. 创建共享实例
const worker = new SharedWorker("./worker.js");

// 2. 通过端口对象的 start 方法显式打开端口连接,因为下文没有使用 onmessage 事件
worker.port.start();

button.addEventListener("click", () => {
  // 3. 通过端口对象发送消息
  worker.port.postMessage([number1.value, number2.value]);
});

// 8. 监听共享线程返回的结果
worker.port.addEventListener("message", e => {
  result.textContent = e.data;
  console.log("执行完毕");
});
复制代码
// worker.js

// 4. 通过 onconnect 事件监听端口连接
onconnect = function (e) {
  // 5. 使用事件对象的 ports 属性,获取端口
  const port = e.ports[0];

  // 6. 通过端口对象的 onmessage 事件监听主线程发送过来的消息,并隐式打开端口连接
  port.onmessage = function (e) {
    console.log("开始后台任务");
    const result= e.data[0] * e.data[1];
    console.log("计算结束");
    console.log(this);

    // 7. 通过端口对象返回结果到主线程
    port.postMessage(result);
  }
}
复制代码

终止 Worker

如果不需要 Worker 继续运行,我们可以在主线程中调用 Worker 实例的 terminate 方法或者使用 Worker 线程的 close 方法来终止 Worker 线程。

Demo

// main.js

const number1 = document.querySelector('#number1');
const number2 = document.querySelector('#number2');
const button = document.querySelector('#button');
const terminate = document.querySelector('#terminate');
const close = document.querySelector('#close');
const result = document.querySelector('#result');

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

button.addEventListener('click', () => {
  worker.postMessage([number1.value, number2.value]);
});

// 主线程中终止 Worker 线程
terminate.addEventListener('click', () => {
  worker.terminate();
  console.log('主线程中终止 Worker 线程');
});

// 发送消息让 Worker 线程自己关闭
close.addEventListener('click', () => {
  worker.postMessage('close');
  console.log('Worker 线程自己关闭');
});

worker.addEventListener('message', e => {
  result.textContent = e.data;
  console.log('执行完毕');
});
复制代码
// worker.js

onmessage = e => {
  if (typeof e.data === 'string' && e.data === 'close') {
    close();
    return;
  }

  console.log('开始后台任务');
  const result= +e.data[0]+ +e.data[1];
  console.log('计算结束');

  postMessage(result);
};
复制代码

处理错误

当 Worker 线程在运行过程中发生错误时,我们在主线程通过 Worker 实例的 error 事件可以接收到 Worker 线程抛出的错误;error 事件的回调函数会返回 ErrorEvent 对象,我们主要关心它的三个属性:

  • filename,发生错误的脚本文件名。
  • lineno,发生错误时所在脚本文件的行号。
  • message,可读性良好的错误消息。

Demo

// main.js

const button = document.querySelector('#button');

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

button.addEventListener('click', () => {
  console.log('主线程发送消息,让 Worker 线程触发错误');
  worker.postMessage('send');
});

worker.addEventListener('error', e => {
  console.log('主线程接收错误,错误消息:');
  console.log('filename:', e.filename);
  console.log('lineno:', e.lineno);
  console.log('message:', e.message);
});
复制代码
// worker.js

onmessage = e => {
  // 利用未声明的变量触发错误
  console.log('Worker 线程利用未声明的 x 变量触发错误');
  postMessage(x * 10);
};
复制代码

生成 Sub Worker

Worker 线程本身也能创建 Worker,这样的 Worker 线程被称为 Sub Worker,它们必须与当前页面同源。另外,在创建 Sub Worker 时传入的地址是相对与当前 Worker 线程而不是页面地址,因为这样有助于记录依赖关系。

Demo

// main.js

const button = document.querySelector('#button');

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

button.addEventListener('click', () => {
  console.log('主线程发送消息给 Worker 线程');
  worker.postMessage('send');
});

worker.addEventListener('message', e => {
  console.log('主线程接收到 Worker 线程回复的消息');
});
复制代码
// worker.js

onmessage = e => {
  console.log('Worker 线程接收到主线程发送的消息');
  const subWorker = new Worker('./sub-worker.js');
  console.log('Worker 线程发送消息给 Sub Worker 线程');
  subWorker.postMessage('send');
  subWorker.addEventListener('message', () => {
    console.log('Worker 线程接收到 Sub Worker 线程回复的消息');
    console.log('Worker 线程回复消息给主线程');

    postMessage('reply');
  })
};
复制代码
// sub-worker.js

self.addEventListener('message', e => {
  console.log('Sub Worker 线程接收到 Worker 线程的发送消息');
  console.log('Sub Worker 线程回复消息给 Worker 线程,并销毁自身')
  self.postMessage('reply');
  self.close();
})
复制代码

引入脚本

Worker 线程中提供了 importScripts 函数来引入脚本,该函数接收零个或者多个 URI;需要注意的是,无论引入的资源是何种类型的文件,importScripts 都会将这个文件的内容当作 JavaScript 进行解析。

importScripts 的加载过程和

你可能感兴趣的:(javascript,嵌入式,json)