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