前言

在浏览器中,因为 JavaScript 引擎与 GUI 渲染线程是互斥的,所以当咱们在 JavaScript 中执行一些计算密集型或高提早的工作的时候,会导致页面渲染被阻塞或拖慢。为了解决这个问题,进步用户体验,HTML5 为咱们带来了 Web Workers 这一规范。

概述

作为 HTML5 规范中的一部分,Web Workers 定义了一套 API,容许一段 JavaScript 程序运行在主线程之外的 Worker 线程中。 在主线程运行的同时,Worker 线程能够在后盾独立运行,解决一些计算密集型或高提早的工作,等 Worker 线程实现计算工作,再把后果返回给主线程。从而保障主线程(通常是 UI 线程)不会因而被阻塞或拖慢。

常见的 Web Workers 次要有以下三种类型:

  • Dedicated Workers
  • Shared Workers
  • Service Workers

Dedicated Workers

专用 Workers, 仅能被生成它的脚本所应用,且只能与一个页面渲染过程进行绑定和通信, 不能多 Tab 共享。浏览器的反对状况如下图:

专用 Workers 的根本用法

1. 创立 worker 线程办法:

咱们在主线程 JS 中调用 new 命令,而后实列化 Worker()构造函数,就能够创立一个 Worker 线程了,代码如下所示:

var worker = new Worker("work.js");

Worker() 构造函数的参数是一个脚本文件,该文件就是 Worker 线程须要执行的工作,须要留神的是,因为 Web Workers 有同源限度,因而这个脚本必须从网络或者本地服务器读取。

2. 主过程发送数据

接下来,咱们就能够从主线程向子线程发送音讯了,应用 worker.postMessage()办法,向 Worker 发送音讯。代码如下所示:

worker.postMessage("Hello LeapFE");

worker.postMessage 办法能够承受任何类型的参数,甚至包含二进制数据。

3. Worker 监听函数

Worker 线程外部须要有一个监听函数,监听主线程/其余子线程 发送过去的音讯。监听事件为 message. 代码如下所示:

addEventListener('message', function(e) { postMessage('子线程向主线程发送音讯: ' + e.data); close(); // 敞开本身 });`

子线程接管到主过程发来的数据,而后执行相应的操作,最初把后果再返回给主线程,

4.主过程接收数据

主线程通过 worker.onmessage 指定监听函数,接管子线程传送回来的音讯,代码如下所示:

worker.onmessage = function (event) {  console.log("接管到的音讯为: " + event.data);};

从事件对象的 data 属性中能够获取到 Worker 发送回来的音讯。

如果咱们的 Worker 线程工作实现后,咱们的主线程须要把它敞开掉,代码如下所示:

worker.terminate();

5. importScripts() 办法

Worker 外部如果须要加载其余的脚本的话,咱们能够应用 importScripts() 办法。代码如下所示:

importScripts("a.js");

如果要加载多个脚本的话,代码能够写成这样:

importScripts('a.js', 'b.js', 'c.js', ....);

6. 谬误监听

主线程能够监听 Worker 线程是否产生谬误,如果产生谬误,Worker 线程会触发主线程的 error 事件。

worker.onerror = function (e) {  console.log(e);};

如果是在 Worker 中如果产生谬误的话, 能够通过throw new Error() 将谬误裸露进去,但这个谬误无奈被主线程获取,只能在 Worker 的 console 中看到“谬误未捕捉提醒”的谬误提醒,而不是主线程的 console

// worker.js外部:// ... other codethrow new Error("test error");

Shared Workers

共享 Workers, 能够看作是专用 Workers 的拓展,除了反对专用 Workers 的性能之外,还能够被不同的 window 页面,iframe,以及 Worker 拜访(当然要遵循同源限度),从而进行异步通信。浏览器的反对状况如下图:

共享 Workers 的根本用法

1. 共享 Workers 的创立

创立共享 Workers 能够通过应用 SharedWorker() 构造函数来实现,这个构造函数应用 URL 作为第一个参数,即是指向 JavaScript 资源文件的 URL。代码如下所示:

var worker = new SharedWorker("sharedworker.js");

2. 共享 Workers 与主过程通信

共享 Workers 与主线程交互的步骤和专用 Worker 根本一样,只是多了一个 port:

// 主线程:const worker = new SharedWorker("worker.js");const key = Math.random().toString(32).slice(-6);worker.port.postMessage(key);worker.port.onmessage = (e) => {  console.log(e.data);};
// worker.js:const buffer = [];onconnect = function (evt) {  const port = evt.ports[0];  port.onmessage = (m) => {    buffer.push(m.data);    port.postMessage("worker receive:" + m.data);  };};

在下面的代码中,须要留神的中央有两点:

  1. onconnect 当其余线程创立 sharedWorker 其实是向 sharedWorker 发了一个链接,worker 会收到一个 connect 事件
  2. evt.ports[0] connect 事件的句柄中 evt.ports[0]是十分重要的对象 port,用来向对应线程发送音讯和接管对应线程的音讯

Service workers

在目前阶段,Service Worker 的次要能力集中在网络代理和离线缓存上。具体的实现上,能够了解为 Service Worker 是一个能在网页敞开时依然运行的 Web Worker。浏览器的反对状况如下图:

PS: Service Workers波及的性能点比拟多,因篇幅无限,本文将暂不进行介绍,咱们会在前面的更新中再具体解析。

专用 Worker 和 共享 Worker 的利用场景

如上文曾经提到的,Worker 能够在后盾独立运行,不阻塞主过程,最常见的应用 Worker 的场景就是解决一些计算密集型或高提早的工作。

场景一: 应用 专用 Worker 来解决耗时较长的问题

咱们在页面中有一个 input 输入框,用户须要在该输入框中输出数字,而后点击旁边的计算按钮,在后盾计算从 1 到给定数值的总和。如果咱们不应用 Web Workers 来解决该问题的话,如下 demo 代码所示:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <title>Web Worker</title>  </head>  <body>    <h1>从1到给定数值的求和</h1>    输出数值: <input type="text" id="num" />    <button onclick="calculate()">计算</button>    <script type="text/javascript">      function calculate() {        var num = parseInt(document.getElementById("num").value, 10);        var result = 0;        // 循环计算求和        for (var i = 0; i <= num; i++) {          result += i;        }        alert("总和为:" + result + "。");      }    </script>  </body></html>

如上代码,而后咱们输出 1 百亿,而后让计算机去帮咱们计算,计算的工夫应该要 20 秒左右的工夫,然而在这 20 秒之前的工夫,那么咱们的页面就处于卡顿的状态,也就是说什么都不能做,等计算结果进去后,咱们就会看到如下弹窗提醒后果了,如下所示:

那当初咱们尝试应用 Web Workers 来解决该问题,把这些耗时操作应用 Worker 去解决,那么主线程就不影响页面假死的状态了,咱们首先把 index.html 代码改成如下:

<!DOCTYPE html><html lang="en">  <head>    <meta charset="UTF-8" />    <title>Web Worker</title>  </head>  <body>    <h1>从1到给定数值的求和</h1>    输出数值: <input type="text" id="num" />    <button id="calculate">计算</button>    <script type="module">      // 创立 worker 实列      var worker = new Worker("./worker1.js");      var calDOM = document.getElementById("calculate");      calDOM.addEventListener("click", calculate);      function calculate() {        var num = parseInt(document.getElementById("num").value, 10);        // 将咱们的数据传递给 worker 线程,让咱们的 worker 线程去帮咱们做这件事        worker.postMessage(num);      }      // 监听 worker 线程的后果      worker.onmessage = function (e) {        alert("总和值为:" + e.data);      };    </script>  </body></html>

如上代码咱们运行下能够看到,咱们点击下计算按钮后,咱们应用主线程把该简单的耗时操作给子线程解决后,咱们点击按钮后,咱们的页面就能够操作了,因为主线程和 Worker 线程是两个不同的环境,Worker 线程的不会影响主线程的。因而如果咱们须要解决一些耗时操作的话,咱们能够应用 Web Workers 线程去解决该问题。

场景二: 应用 共享 Worker 实现跨页面数据共享

上面咱们给出一个例子:创立一个共享 Worker 共享多个 Tab 页的数据,实现一个简略的网页聊天室的性能。

首先在 index.html 中设计简略的聊天对话框款式, 同时引入 main.js:

<!DOCTYPE html><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>Shared Worker Example</title>    <style>      ul li {        float: left;        list-style: none;        margin-top: 10px;        width: 100%;      }      ul li > span {        font-size: 10px;        transform: scale(0.8);        display: block;        width: 17%;      }      ul li > p {        background: rgb(140, 222, 247);        border-radius: 4px;        padding: 4px;        margin: 0;        display: inline-block;      }      ul li.right {        float: right;        text-align: right;      }      ul li.right > p {        background: rgb(132, 226, 140);      }      ul li.right > span {        width: 110%;      }      #chatList {        width: 300px;        background: #fff;        height: 400px;        padding: 10px;        border: 4px solid #de8888;        border-radius: 10px;      }    </style>  </head>  <body>    <div class="container">      <section>        <p id="user"></p>        <ul id="chatList" style="width: 300px"></ul>        <input id="input" />        <button id="submitBtn">提交</button>      </section>    </div>    <script src="./main.js"></script>  </body></html>

在 main.js 中,咱们初始化一个 SharedWorker 实例

window.onload = () => {  const worker = new SharedWorker("./shared-worker.js");  const chatList = document.querySelector("#chatList");  let id = null;  worker.port.onmessage = (event) => {    const { data } = event;    switch (data.action) {      case "id": // 接管 Worker 实例化胜利之后返回的 id        id = data.value;        document.querySelector("#user").innerHTML = `Client ${id}`;        break;      case "message": // 接管 Worker 返回的来自各个页面的信息        chatList.innerHTML += `<li class="${          data.id === id ? "right" : "left"        }"><span>Client ${data.id}</span><p>${data.value}</p></li>`;        break;      default:        break;    }  };  document.querySelector("#submitBtn").addEventListener("click", () => {    const value = document.querySelector("#input").value;    // 将以后用户 ID 及音讯发送给 Worker    worker.port.postMessage({      action: "message",      value: value,      id,    });  });};

shared-worker.js 接管与各页面的连贯,同时转发页面发送过去的音讯

const connectedClients = new Set();let connectID = 1;function sendMessageToClients(payload) {  //将音讯分发给各个页面  connectedClients.forEach(({ client }) => {    client.postMessage(payload);  });}function setupClient(clientPort) {  //通过 onmessage 监听来自主过程的音讯  clientPort.onmessage = (event) => {    const { id, value } = event.data;    sendMessageToClients({      action: "message",      value: value,      id: connectID,    });  };}// 通过 onconnect 函数监听,来自不同页面的 Worker 连贯onconnect = (event) => {  const newClient = event.ports[0];  // 保留连贯到 Worker 的页面援用  connectedClients.add({    client: newClient,    id: connectID,  });  setupClient(newClient);  // 页面同 Worker 连贯胜利后, 将以后连贯的 ID 返回给页面  newClient.postMessage({    action: "id",    value: connectID,  });  connectID++;};

在下面的共享线程例子中,在主页面即各个用户连贯页面结构出一个共享线程对象,而后通过 worker.port.postMessage 向共享线程发送用户输出的信息。同时,在共享线程的实现代码片段中定义 connectID, 用来记录连贯到这个共享线程的总数。之后,用 onconnect 事件处理器接管来自不同用户的连贯,解析它们传递过去的信息。最初,定义一个了办法 sendMessageToClients 将音讯分发给各个用户。

总结

Web Workers 的确给咱们提供了优化 Web 利用的新可能,通过应用 Web Workers 来正当地调度 JavaScript 运行逻辑,能够在面对无奈预测的低端设施和长工作时,保障 GUI 仍旧是可响应的。

或者将来,应用 Web Workers 进行编程或者会成为新一代 Web 利用开发的标配或是最佳实际。