最初的 JS 執行代碼都是一條線執行到底,當遇到比較耗時的操作時,比如大數組循環運算,就會導致頁面卡着,就像假死一樣。就像一個人在廚房燒菜一樣,需要依次完成切菜、炒菜、裝盤這些步驟,此過程中沒辦法同時做其他事情,必須按順序執行每一個步驟。
Web Worker 賦予了 JS 分配任務的能力,在遇到複雜的計算型任務時,比如 canvas 圖形圖像處理(添加濾鏡、矩陣變換等),此類不依賴 DOM 操作的計算型任務都可以交由 Web Worker 來處理,這樣不會阻塞主線程的任務調度,從而提升前端的代碼運行速度。
任務時序圖

模擬耗時任務
看如下代碼,使用一個超大的 for 循環,模擬 JS 中的耗時任務,讓代碼執行時主線程卡頓,還原假死現象:
<div id="output"></div>
<button id="start">開始複雜任務</button>
<script>
(() => {
let times = 0
const output = document.getElementById('output')
function loop () {
setTimeout(() => {
times ++
output.innerText = times
loop()
}, 1000);
}
loop()
document.getElementById('start').addEventListener('click', () => {
let n = 0
console.time('任務耗時')
for (let i = 0; i < 10000000000; i++) {
n += i
}
console.timeEnd('任務耗時')
})
})();
</script>
執行結果:

可以看到點擊 開始複雜任務 按鈕時,在計時器的第 4 秒主線程卡主了將近 4 秒,然後再恢復運行,這就是單線程中的 JS 耗時任務導致的頁面假死現象。
使用 Web Worker 解決耗時問題
看了上面的耗時任務導致頁面假死,再使用 Web Worker 來重寫一下上面代碼:
main.html
<div id="output"></div>
<button id="start">開始複雜任務</button>
<script>
(() => {
let times = 0
const output = document.getElementById('output')
function loop () {
setTimeout(() => {
times ++
output.innerText = times
loop()
}, 1000);
}
loop()
const worker = new Worker('./worker.js')
worker.onmessage = event => {
// 子線程計算結果
console.log(event.data)
console.timeEnd('任務耗時')
}
worker.onerror = event => {
console.error('Worker 異常:', event)
}
document.getElementById('start').addEventListener('click', () => {
console.time('任務耗時')
worker.postMessage(10000000000)
})
})();
</script>
worker.js
// worker.js
self.onmessage = event => {
let n = 0
let max = event.data
for (let i = 0; i < max; i++) {
n += i
}
postMessage(n)
}
執行結果:

可以看到雖然任務耗時長短差不多,但是主線程在點擊按鈕之後並沒有進入假死狀態,定時器還是在順利執行,所以 Web Worker 中運行的複雜任務並不會影響主線程的任務調度。
Web Worker 限制
在子線程中運行的代碼,無法直接操作 DOM,無法訪問 window/document 對象,也無法使用 localStorage 等,如果使用這些 API,代碼將會報錯:

for 循環優化
注意上述代碼中的 max 變量,為什麼需要一個變量來保存 event.data 值?而不是直接使用 event.data 循環?將 worker.js 改造一下,看看不同使用方式的任務耗時:
// worker.js
self.onmessage = event => {
console.time('max 循環耗時')
let n = 0
let max = event.data
for (let i = 0; i < max; i++) {
n += i
}
console.timeEnd('max 循環耗時')
console.time('Object 循環耗時')
let m = 0
for (let i = 0; i < event.data; i++) {
m += i
}
console.timeEnd('Object 循環耗時')
postMessage(n)
}
main.html
// main.html
(() => {
const worker = new Worker('./worker.js')
document.getElementById('start').addEventListener('click', () => {
worker.postMessage(100000000)
})
})();
執行結果:

可以明顯看到,性能耗時相差將近 6 倍,這數字會隨着對象屬性越多,耗時越長!!所以在循環中應當儘量避免讀取對象屬性,儘可能使用變量來做循環條件!!
寫在最後
可以使用 Web Worker 同時啓用多個工作線程,只是在任務調度的時候,需要注意響應結果的先後順序是否對主線程的運行有影響。
一些複雜的計算任務(比如視頻轉碼,圖片壓縮,圖片處理等),都丟給子線程處理吧,咱們前端也可以玩玩多線程~~