淺析 Web Workers 及 應用

前言

在瀏覽器中,因爲 JavaScript 引擎與 GUI 渲染線程是互斥的,因此當咱們在 JavaScript 中執行一些計算密集型或高延遲的任務的時候,會致使頁面渲染被阻塞或拖慢。爲了解決這個問題,提升用戶體驗,HTML5 爲咱們帶來了 Web Workers 這一標準。javascript

概述

做爲 HTML5 標準中的一部分,Web Workers 定義了一套 API,容許一段 JavaScript 程序運行在主線程以外的 Worker 線程中。 在主線程運行的同時,Worker 線程能夠在後臺獨立運行,處理一些計算密集型或高延遲的任務,等 Worker 線程完成計算任務,再把結果返回給主線程。從而保證主線程(一般是 UI 線程)不會所以被阻塞或拖慢。html

常見的 Web Workers 主要有如下三種類型:java

  • Dedicated Workers
  • Shared Workers
  • Service Workers

Dedicated Workers

專用 Workers, 僅能被生成它的腳本所使用,且只能與一個頁面渲染進程進行綁定和通訊, 不能多 Tab 共享。瀏覽器的支持狀況以下圖:
https://s1.firstleap.cn/s/104395/083621334062269571618899799914.png編程

專用 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. 代碼以下所示:dom

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 訪問(固然要遵循同源限制),從而進行異步通訊。瀏覽器的支持狀況以下圖:
https://s1.firstleap.cn/s/104395/92874481873100351618899810253.png

共享 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。瀏覽器的支持狀況以下圖:
https://s1.firstleap.cn/s/104395/330800979133963671618899806918.png

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 頁的數據,實現一個簡單的網頁聊天室的功能。

https://s1.firstleap.cn/s/104395/33920652922595031618927790538.png

首先在 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 應用開發的標配或是最佳實踐。

相關文章
相關標籤/搜索