本文是我的上一篇文章《PWA一隅》 的延伸,介紹與 PWA 中 Service Worker 相關的 Web Worker,希望對你有所幫助,也請多多指教。圍繞 PWA 主題的文章還將陸續釋出,敬請期待。
簡介
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()
一致。
var sharedWorker = new SharedWorker('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)的後臺任務將會被刪除。
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
相關連結
參考
- 使用 Web Workers - Web APIs | MDN
- Worker | MDN
- MessagePort | MDN
- HTML Standard - Web workers
- Web Workers 的基本資訊