关于javascript:浅析-Web-Workers-及-应用

前言

在浏览器中,因为 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 code
throw 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 利用开发的标配或是最佳实际。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理