能夠不會用但你必需要了解的Web Worker詳解

相關係列: 從零開始的前端築基之旅(面試必備,持續更新~)javascript

Javascript是運行在單線程環境中,也就是說沒法同時運行多個腳本。假設用戶點擊一個按鈕,觸發了一段用於計算的Javascript代碼,那麼在這段代碼執行完畢以前,頁面是沒法響應用戶操做的。html

簡介

Web Worker爲Web內容在後臺線程中運行腳本提供了一種簡單的方法。線程能夠執行任務而不干擾用戶界面。前端

Web Worker (工做線程) 是 HTML5 中提出的概念,它讓咱們能夠在頁面運行的 JavaScript 主線程中加載運行另外單獨的一個或者多個 JavaScript 線程;java

Web Worker分爲兩種類型,專用線程(Dedicated Web Worker) 和共享線程(Shared Web Worker)。專用線程僅能被建立它的腳本所使用(一個專用線程對應一個主線程),而共享線程可以在不一樣的腳本中使用(一個共享線程對應多個主線程)。git

Web Worker 提供的多線程編程能力與們傳統意義上的多線程編程(Java、C++ 等)不一樣,主程序線程和 Worker 線程之間,Worker 線程之間,不會共享任何做用域或資源,它們間惟一的通訊方式就是一個基於事件監聽機制的 message。github

JavaScript 語言自己還是運行在單線程上的, Web Worker 只是瀏覽器(宿主環境)提供的一個能力/API。web

應用場景

Web Worker 的實現爲前端程序帶來了後臺計算的能力,咱們能夠將一些耗時的數據處理操做從主線程中剝離,從而極大減輕了因計算量大形成 UI 阻塞而出現的界面渲染卡、掉幀的狀況,使主線程更加專一於頁面渲染和交互,更大程度地利用了終端硬件的性能;面試

  • 數學運算
  • 大數據處理
  • 懶加載
  • 文本分析
  • 流媒體數據處理
  • canvas 圖形繪製
  • 圖像處理
  • ...

注意

  1. 在worker內,不能直接操做DOM節點,也不能使用window對象的默認方法和屬性。可是可使用WebSockets,IndexedDB以及FireFox OS專用的Data Store API等數據存儲機制
  2. workers和主線程間經過postMessage()方法發送各自的消息,使用onmessage事件處理函數來響應消息(消息被包含在Message事件的data屬性中)。這個過程當中數據並非被共享而是被複製
  3. Web Worker 的運行不會影響主線程,但與主線程交互時仍受到主線程單線程的瓶頸制約。換言之,若是 Worker 線程頻繁與主線程進行交互,主線程因爲須要處理交互,仍有可能使頁面發生阻塞
  4. 共享線程能夠被多個瀏覽上下文(Browsing context)調用,但全部這些瀏覽上下文必須同源(相同的協議,主機和端口號)

只要運行在同源的父頁面中,workers能夠依次生成新的workers;編程

基本知識瞭解了,下面進行枯燥的使用講解。canvas

專用worker(Dedicated Web Worker)

建立worker

建立一個新的worker很簡單。調用Worker() 的構造器,指定一個腳本的URI來執行worker線程(main.js):

const myWorker = new Worker('worker.js');
複製代碼

worker檢測

爲了更好的錯誤處理控制以及向下兼容,將worker運行代碼包裹在如下代碼中是一個很好的想法(main.js):

if (window.Worker) {

  ...

}
複製代碼

消息的接收和發送

Worker 線程和主線程都經過 postMessage() 方法發送消息,經過 onmessage 事件接收消息。

在主線程中使用時,onmessagepostMessage() 必須掛在worker對象上,而在worker中使用時不須要這樣作。在worker內部,selfthis 都表明子線程的全局對象。

對於監聽 message 事件,如下四種寫法是等同的。

// 寫法 1
self.addEventListener('message', function (e) {
    // ...
})

// 寫法 2
this.addEventListener('message', function (e) {
    // ...
})

// 寫法 3
addEventListener('message', function (e) {
    // ...
})

// 寫法 4
onmessage = function (e) {
    // ...
}
複製代碼

在主頁面與 worker 之間傳遞的數據是經過拷貝,而不是共享來完成的。

// main.js
const myWorker = new Worker('worker.js')

myWorker.onmessage = function(e) {
    console.log(e.data) // 24
}

myWorker.postMessage([10, 24])


// Worker.js
onmessage = function (e) {
    const data = e.data
    postMessage(data[0] + data[1])
}
複製代碼

傳遞給 worker 的對象須要通過序列化,接下來在另外一端還須要反序列化。頁面與 worker **不會共享同一個實例,最終的結果就是在每次通訊結束時生成了數據的一個副本。**大部分瀏覽器使用結構化拷貝來實現該特性。

worker線程修改data數據不影響主線程中原始對象

經過轉讓全部權(可轉讓對象)來傳遞數據

另外一種性能更高的方法是將特定類型的對象(可轉讓對象) 傳遞給一個 worker/從 worker 傳回 。可轉讓對象從一個上下文轉移到另外一個上下文而不會通過任何拷貝操做。這意味着當傳遞大數據時會得到極大的性能提高。

與按照引用傳遞不一樣的是,一旦對象轉讓,那麼它在原來上下文的那個版本將不復存在。該對象的全部權被轉讓到新的上下文內。例如,當你將一個 ArrayBuffer 對象從主應用轉讓到 Worker 中,原始的 ArrayBuffer 被清除而且沒法使用。它包含的內容會(完整無差的)傳遞給 Worker 上下文。

var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
for (var i = 0; i < uInt8Array .length; ++i) {
  uInt8Array[i] = i;
}
const myWorker = new Worker('worker.js')

myWorker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

console.log(uInt8Array.length); // 傳遞後長度:0
複製代碼

關閉 Worker

能夠調用worker的terminate 方法從主線程中馬上終止一個運行中的worker:

myWorker.terminate();
複製代碼

worker 線程會被當即殺死,不會有任何機會讓它完成本身的操做或清理工做。

worker線程內也能夠調用本身的 close 方法進行關閉:

close();
複製代碼

錯誤處理

能夠經過在主線程或 Worker 線程中設置 onerroronmessageerror 的回調函數對錯誤進行處理。

當 worker 出現運行中錯誤時,它的 onerror 事件處理函數會被調用。它會收到一個擴展了 ErrorEvent 接口的名爲 error的事件。

該事件不會冒泡而且能夠被取消;爲了防止觸發默認動做,worker 能夠調用錯誤事件的 preventDefault()方法。

// main.js
myWorker.onerror = function () {
    // ...
}
myWorker.onmessageerror = function () {
    // ...
}

// worker.js
onerror = function () {

}
複製代碼

錯誤事件有如下三個字段:

  • message可讀性良好的錯誤消息。
  • filename發生錯誤的腳本文件名。
  • lineno發生錯誤時所在腳本文件的行號。

生成subworker

若是須要的話 worker 可以生成更多的 worker。這就是所謂的subworker,它們必須託管在同源的父頁面內。並且,subworker 解析 URI 時會相對於父 worker 的地址而不是自身頁面的地址。這使得 worker 更容易記錄它們之間的依賴關係。

引入腳本與庫

Worker 線程可以訪問一個全局函數importScripts()來引入腳本,該函數接受0個或者多個URI做爲參數來引入資源;如下例子都是合法的:

importScripts();                        /* 什麼都不引入 */
importScripts('foo.js');                /* 只引入 "foo.js" */
importScripts('foo.js', 'bar.js');      /* 引入兩個腳本 */
複製代碼

腳本的下載順序不固定,但執行時會按照傳入 importScripts() 中的文件名順序進行。

嵌入式 Worker

目前沒有一類標籤可使 Worker 的代碼像 <script> 元素同樣嵌入網頁中,可是若是一個 <script> 元素沒有 src 特性,而且它的 type 特性沒有指定成一個可運行的 mime-type,那麼它就會被認爲是一個數據塊元素,而且可以被 JavaScript 使用。咱們能夠經過 Blob() 將頁面中的 Worker 代碼進行解析。

<script id="worker" type="javascript/worker">
// 這段代碼不會被 JS 引擎直接解析,由於類型是 'javascript/worker'

// 在這裏寫 Worker 線程的邏輯
</script>
<script>
    var workerScript = document.querySelector('#worker').textContent
    var blob = new Blob(workerScript, {type: "text/javascript"})
    var worker = new Worker(window.URL.createObjectURL(blob))
</script>
複製代碼

固然,你也能夠經過下面方式來使用:

var myTask = ` onmessage = function (e) { var data = e.data; console.log('worker:', data); }; `;

var blob = new Blob([myTask]);
var myWorker = new Worker(window.URL.createObjectURL(blob));
複製代碼

Worker上下文(WorkerGlobalScope)

workers 運行在另外一個全局上下文中,不一樣於當前的window.

  • 獲取window會報錯
  • 試圖操縱dom會報錯

專用workers的狀況下,

  • DedicatedWorkerGlobalScope 對象表明了worker的上下文。
  • 該 DedicatedWorkerGlobalScope 對象(也就是 Worker 全局做用域)能夠經過 self關鍵字來訪問 。
  • 每個 DedicatedWorkerGlobalScope 對象都有不一樣的 event loop。這個 event loop 沒有關聯瀏覽器上下文(browsing context),它的任務隊列也只有事件(events)、回調(callbacks)和聯網的活動(networking activity)。

除了標準的 JavaScript 函數集 (例如 String, Array, Object, JSON 等), DOM有多種功能可供 workers使用。DedicatedWorkerGlobalScope像Window,實現WindowTimersWindowBase64

導航相關

  • Navigator
  • Location

時間相關

  • clearInterval()
  • clearTimeout()
  • setInterval()
  • setTimeout

存儲相關

  • Cache
  • IndexedDB

網絡相關

  • Fetch
  • WebSocket
  • XMLHttpRequest

其餘

  • console
  • performance

共享worker(Shared Web Worker)

一個共享worker能夠被多個腳本使用。

共享worker能夠被多個瀏覽上下文調用,全部這些瀏覽上下文必須屬於同源(相同的協議,主機和端口號)。在本地調試的時候也須要經過啓動本地服務器的方式訪問,使用 file:// 協議直接打開的話將會拋出異常。

建立worker

生成一個新的共享worker與生成一個專用worker很是類似,只是構造器的名字不一樣

const myWorker = new SharedWorker('worker.js');
複製代碼

與一個共享worker通訊必須經過端口對象——一個確切的打開的端口供腳本與worker通訊

在傳遞消息以前,端口鏈接必須被顯式的打開,打開方式是使用onmessage事件處理函數或者start()方法。

start() 方法是與 addEventListener 配套使用的。若是咱們選擇 onmessage 進行事件監聽,那麼將隱含調用 start() 方法。

消息的接收和發送

postMessage() 方法必須被端口對象調用

myWorker.port.postMessage([squareNumber.value,squareNumber.value]);
複製代碼

相比於專用Worker,多了個全局的 connect() 函數,在函數中須要去獲取一個 post 對象來進行初始化操做;

onconnect = function(e) {
  var port = e.ports[0];

  port.onmessage = function(e) {
    var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
    port.postMessage(workerResult);
  }
}
複製代碼
  1. 當一個端口鏈接被建立時(例如:在父級線程中,設置onmessage事件處理函數,或者顯式調用start()方法時),使用onconnect事件處理函數來執行代碼。
  2. 使用事件的ports屬性來獲取端口並存儲在變量中。
  3. 而後,爲端口添加一個消息處理函數用來作運算並回傳結果給主線程。

關於線程安全

Worker接口會生成真正的操做系統級別的線程,對於 web worker 來講,與其餘線程的通訊點會被很當心的控制,這意味着你很難引發併發問題。你沒有辦法去訪問非線程安全的組件或者是 DOM,此外你還須要經過序列化對象來與線程交互特定的數據。因此你要是不費點勁兒,還真搞不出錯誤來。

其它類型的worker

除了專用和共享的web worker,還有一些其它類型的worker:

  • ServiceWorkers (服務worker)通常做爲web應用程序、瀏覽器和網絡(若是可用)以前的代理服務器。它們旨在(除開其餘方面)建立有效的離線體驗,攔截網絡請求,以及根據網絡是否可用採起合適的行動並更新駐留在服務器上的資源。他們還將容許訪問推送通知和後臺同步API。
  • Chrome Workers 是一種僅適用於firefox的worker。若是您正在開發附加組件,但願在擴展程序中使用worker且有在你的worker中訪問 js-ctypes 的權限,你可使用Chrome Workers。詳情請參閱ChromeWorker
  • Audio Workers (音頻worker)使得在web worker上下文中直接完成腳本化音頻處理成爲可能。

若是你收穫了新知識,請在作側邊欄第一個按鈕用力點一下~

參考文章:

  1. JavaScript 性能利器 —— Web Worker
  2. 淺談HTML5 Web Worker
  3. 使用 Web Workers
相關文章
相關標籤/搜索