JavaScript 性能利器 —— Web Worker

本文創做於 2018-12-12,2019-12-20 遷移至此

簡介

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

專用線程能夠看作是默認狀況的 Web Worker,其加上修飾詞的目的是爲了與共享線程進行區分。本文會較爲嚴格地區分二者,可能較爲累贅,但我的認爲這是必要的。若是單純以 Web Worker 字樣出現的地方指的是二者都會有的狀況。html

用途

Web Worker 的意義在於能夠將一些耗時的數據處理操做從主線程中剝離,使主線程更加專一於頁面渲染和交互。html5

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

須要注意的點

  • 有同源限制
  • 沒法訪問 DOM 節點
  • 運行在另外一個上下文中,沒法使用Window對象
  • Web Worker 的運行不會影響主線程,但與主線程交互時仍受到主線程單線程的瓶頸制約。換言之,若是 Worker 線程頻繁與主線程進行交互,主線程因爲須要處理交互,仍有可能使頁面發生阻塞
  • 共享線程能夠被多個瀏覽上下文(Browsing context)調用,但全部這些瀏覽上下文必須同源(相同的協議,主機和端口號)

瀏覽器支持度

根據 CanI Use 網站的統計,目前約有 93.05% 的瀏覽器支持專用線程。
16726223java

而對於共享線程,則僅有大約 41.66% 的瀏覽器支持。
502174web

因爲專用線程和共享線程的構造方法都包含在 window 對象中,咱們在使用二者以前能夠對瀏覽器的支持性進行判斷。算法

if (window.Worker) {
    // ...
}
if (window.SharedWorker) {
    // ...
}

線程建立

專用線程由 Worker()方法建立,能夠接收兩個參數,第一個參數是必填的腳本的位置,第二個參數是可選的配置對象,能夠指定 typecredentialsname 三個屬性。canvas

var worker = new Worker('worker.js')
// var worker = new Worker('worker.js', { name: 'dedicatedWorker'})

共享線程使用 Shared Worker() 方法建立,一樣支持兩個參數,用法與 Worker() 一致。數組

var sharedWorker = new SharedWorker('shared-worker.js')

值得注意的是,由於 Web Worker 有同源限制,因此在本地調試的時候也須要經過啓動本地服務器的方式訪問,使用 file:// 協議直接打開的話將會拋出異常。瀏覽器

數據傳遞

Worker 線程和主線程都經過 postMessage() 方法發送消息,經過 onmessage 事件接收消息。在這個過程當中數據並非被共享的,而是被複制的。值得注意的是 ErrorFunction 對象不能被結構化克隆算法複製,若是嘗試這麼作的話會致使拋出 DATA_CLONE_ERR 的異常。另外,postMessage() 一次只能發送一個對象, 若是須要發送多個參數能夠將參數包裝爲數組或對象再進行傳遞。服務器

關於 postMessage() 和結構化克隆算法(The structured clone algorithm)將在本文最後進行闡述。

下面是專用線程數據傳遞的示例。

// 主線程
var worker = new Worker('worker.js')
worker.postMessage([10, 24])
worker.onmessage = function(e) {
    console.log(e.data)
}

// Worker 線程
onmessage = function (e) {
    if (e.data.length > 1) {
        postMessage(e.data[1] - e.data[0])
    }
}

在 Worker 線程中,selfthis 都表明子線程的全局對象。對於監聽 message 事件,如下的四種寫法是等同的。

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

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

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

// 寫法 4
onmessage = function (e) {
    // ...
}

主線程經過 MessagePort 訪問專用線程和共享線程。專用線程的 port 會在線程建立時自動設置,而且不會暴露出來。與專用線程不一樣的是,共享線程在傳遞消息以前,端口必須處於打開狀態。MDN 上的 MessagePort 關於 start() 方法的描述是:

Starts the sending of messages queued on the port (only needed when using EventTarget.addEventListener; it is implied when using MessagePort.onmessage.)

這句話通過試驗,能夠理解爲 start() 方法是與 addEventListener 配套使用的。若是咱們選擇 onmessage 進行事件監聽,那麼將隱含調用 start() 方法。

// 主線程
var sharedWorker = new SharedWorker('shared-worker.js')
sharedWorker.port.onmessage = function(e) {
    // 業務邏輯
}
var sharedWorker = new SharedWorker('shared-worker.js')
sharedWorker.port.addEventListener('message', function(e) {
    // 業務邏輯
}, false)
sharedWorker.port.start() // 須要顯式打開

在傳遞消息時,postMessage() 方法和 onmessage 事件必須經過端口對象調用。另外,在 Worker 線程中,須要使用 onconnect 事件監聽端口的變化,並使用端口的消息處理函數進行響應。

// 主線程
sharedWorker.port.postMessage([10, 24])
sharedWorker.port.onmessage = function (e) {
    console.log(e.data)
}

// Worker 線程
onconnect = function (e) {
    let port = e.ports[0]

    port.onmessage = function (e) {
        if (e.data.length > 1) {
            port.postMessage(e.data[1] - e.data[0])
        }
    }
}

關閉 Worker

能夠在主線程中使用 terminate() 方法或在 Worker 線程中使用 close() 方法關閉 worker。這兩種方法是等效的,但比較推薦的用法是使用 close(),防止意外關閉正在運行的 Worker 線程。Worker 線程一旦關閉 Worker 後 Worker 將再也不響應。

// 主線程
worker.terminate()

// Dedicated Worker 線程中
self.close()

// Shared Worker 線程中
self.port.close()

錯誤處理

能夠經過在主線程或 Worker 線程中設置 onerroronmessageerror 的回調函數對錯誤進行處理。其中,onerror 在 Worker 的 error 事件觸發並冒泡時執行,onmessageerror 在 Worker 收到的消息不能進行反序列化時觸發(本人通過嘗試沒有辦法觸發 onmessageerror 事件,若是在 worker 線程使用 postMessage 方法傳遞一個 Error 或 Function 對象會由於沒法序列化優先被 onerror 方法捕獲,而根本不會進入反序列化的過程)。

// 主線程
worker.onerror = function () {
    // ...
}

// 主線程使用專用線程
worker.onmessageerror = function () {
    // ...
}

// 主線程使用共享線程
worker.port.onmessageerror = function () {
    // ...
}

// worker 線程
onerror = function () {

}

加載外部腳本

Web Worker 提供了 importScripts() 方法,可以將外部腳本文件加載到 Worker 中。

importScripts('script1.js')
importScripts('script2.js')

// 以上寫法等價於
importScripts('script1.js', 'script2.js')

子線程

Worker 能夠生成子 Worker,但有兩點須要注意。

  • 子 Worker 必須與父網頁同源
  • 子 Worker 中的 URI 相對於父 Worker 所在的位置進行解析

嵌入式 Worker

目前沒有一類標籤可使 Worker 的代碼像 <script> 元素同樣嵌入網頁中,但咱們能夠經過 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>

關於 postMessage

Web Worker 中,Worker 線程和主線程之間使用結構化克隆算法(The structured clone algorithm)進行數據通訊。結構化克隆算法是一種經過遞歸輸入對象構建克隆的算法,算法經過保存以前訪問過的引用的映射,避免無限遍歷循環。這一過程能夠理解爲,在發送方使用相似 JSON.stringfy() 的方法將參數序列化,在接收方採用相似 JSON.parse() 的方法反序列化。

可是,一次數據傳輸就須要同時通過序列化和反序列化,若是數據量大的話,這個過程自己也可能形成性能問題。所以, Worker 中提出了 Transferable Objects 的概念,當數據量較大時,咱們能夠選擇在將主線程中的數據直接移交給 Worker 線程。值得注意的是,這種轉移是完全的,一旦數據成功轉移,主線程將不能訪問該數據。這個移交的過程仍然經過 postMessage 進行傳遞。

postMessage(message, transferList)

例如,傳遞一個 ArrayBuffer 對象

let aBuffer = new ArrayBuffer(1)
worker.postMessage({ data: aBuffer }, [aBuffer])

上下文

Worker 工做在一個 WorkerGlobalDataScope 的上下文中。每個 WorkerGlobalDataScope 對象都有不一樣的 event loop。這個 event loop 沒有關聯瀏覽器上下文(browsing context),它的任務隊列也只有事件(events)、回調(callbacks)和聯網的活動(networking activity)。

每個 WorkerGlobalDataScope 都有一個 closing 標誌,當這個標誌設爲 true 時,任務隊列將丟棄以後試圖加入任務隊列的任務,隊列中已經存在的任務不受影響(除非另有指定)。同時,定時器將中止工做,全部掛起(pending)的後臺任務將會被刪除。

Worker 中可使用的函數和類

因爲 Worker 工做的上下文不一樣於普通的瀏覽器上下文,所以不能訪問 window 以及 window 相關的 API,也不能直接操做 DOM。Worker 中提供了 WorkerNavigatorWorkerLocation 接口,它們分別是 window 中 NavigatorLocation 的子集。除此以外,Worker 還提供了涉及時間、存儲、網絡、繪圖等多個種類的接口,如下列舉了其中的一部分,更多的接口能夠參考 MDN 文檔

時間相關

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

Worker 相關

  • importScripts()
  • close()
  • postMessage()

存儲相關

  • Cache
  • IndexedDB

網絡相關

  • Fetch
  • WebSocket
  • XMLHttpRequest

相關連接

參考

擴展閱讀

相關文章
相關標籤/搜索