1. 程式人生 > 實用技巧 >JS效能利器_Web Worker

JS效能利器_Web Worker

簡介

Web Worker (工作執行緒) 是html5 中提出的概念,分為兩種型別,專用執行緒(Dedicated Web Worker) 和共享執行緒(Shared Web Worker)。專用執行緒僅能被建立它的指令碼所使用(一個專用執行緒對應一個主執行緒),而共享執行緒能夠在不同的指令碼中使用(一個共享執行緒對應多個主執行緒)。

專用執行緒可以看做是預設情況的 Web Worker,其加上修飾詞的目的是為了與共享執行緒進行區分。本文會較為嚴格地區分兩者,可能較為累贅,但個人認為這是必要的。如果單純以Web Worker字樣出現的地方指的是兩者都會有的情況。

用途

Web Worker 的意義在於可以將一些耗時的資料處理操作從主執行緒中剝離,使主執行緒更加專注於頁面渲染和互動。

  • 懶載入
  • 文字分析
  • 流媒體資料處理
  • canvas 圖形繪製
  • 影象處理
  • ...

需要注意的點

  • 有同源限制
  • 無法訪問 DOM 節點
  • 執行在另一個上下文中,無法使用Window物件
  • Web Worker 的執行不會影響主執行緒,但與主執行緒互動時仍受到主執行緒單執行緒的瓶頸制約。換言之,如果 Worker 執行緒頻繁與主執行緒進行互動,主執行緒由於需要處理互動,仍有可能使頁面發生阻塞
  • 共享執行緒可以被多個瀏覽上下文(Browsing context)呼叫,但所有這些瀏覽上下文必須同源(相同的協議,主機和埠號)

瀏覽器支援度

根據 CanI Use網站的統計,目前約有 93.05% 的瀏覽器支援專用執行緒。

而對於共享執行緒,則僅有大約 41.66% 的瀏覽器支援。

由於專用執行緒和共享執行緒的構造方法都包含在 window 物件中,我們在使用兩者之前可以對瀏覽器的支援性進行判斷。

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

執行緒建立

專用執行緒由Worker()方法建立,可以接收兩個引數,第一個引數是必填的指令碼的位置,第二個引數是可選的配置物件,可以指定type、credentials、name三個屬性。

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

共享執行緒使用Shared Worker()方法建立,同樣支援兩個引數,用法與Worker()一致。

varsharedWorker =newSharedWorker('shared-worker.js')

值得注意的是,因為 Web Worker 有同源限制,所以在本地除錯的時候也需要通過啟動本地伺服器的方式訪問,使用file://協議直接開啟的話將會丟擲異常。

資料傳遞

Worker 執行緒和主執行緒都通過postMessage()方法傳送訊息,通過onmessage事件接收訊息。在這個過程中資料並不是被共享的,而是被複制的。值得注意的是Error和Function物件不能被結構化克隆演算法複製,如果嘗試這麼做的話會導致丟擲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 執行緒中,self和this都代表子執行緒的全域性物件。對於監聽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 執行緒中設定onerror和onmessageerror的回撥函式對錯誤進行處理。其中,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)的後臺任務將會被刪除。

資源搜尋網站大全 https://www.renrenfan.com.cn 廣州VI設計公司https://www.houdianzi.com

Worker 中可以使用的函式和類

由於 Worker 工作的上下文不同於普通的瀏覽器上下文,因此不能訪問 window 以及 window 相關的 API,也不能直接操作 DOM。Worker 中提供了WorkerNavigator和WorkerLocation介面,它們分別是 window 中Navigator和Location的子集。除此之外,Worker 還提供了涉及時間、儲存、網路、繪圖等多個種類的介面,以下列舉了其中的一部分,更多的介面可以參考MDN 文件。

時間相關

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

Worker 相關

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

儲存相關

  • Cache
  • IndexedDB

網路相關

  • Fetch
  • WebSocket
  • XMLHttpRequest