動態

詳情 返回 返回

Web前端入門第 86 問:JavaScript 中的 Web Worker 為什麼能提升代碼性能? - 動態 詳情

最初的 JS 執行代碼都是一條線執行到底,當遇到比較耗時的操作時,比如大數組循環運算,就會導致頁面卡着,就像假死一樣。就像一個人在廚房燒菜一樣,需要依次完成切菜、炒菜、裝盤這些步驟,此過程中沒辦法同時做其他事情,必須按順序執行每一個步驟。

Web Worker 賦予了 JS 分配任務的能力,在遇到複雜的計算型任務時,比如 canvas 圖形圖像處理(添加濾鏡、矩陣變換等),此類不依賴 DOM 操作的計算型任務都可以交由 Web Worker 來處理,這樣不會阻塞主線程的任務調度,從而提升前端的代碼運行速度。

任務時序圖

1

模擬耗時任務

看如下代碼,使用一個超大的 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>

執行結果:

2

可以看到點擊 開始複雜任務 按鈕時,在計時器的第 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)
}

執行結果:

3

可以看到雖然任務耗時長短差不多,但是主線程在點擊按鈕之後並沒有進入假死狀態,定時器還是在順利執行,所以 Web Worker 中運行的複雜任務並不會影響主線程的任務調度。

Web Worker 限制

在子線程中運行的代碼,無法直接操作 DOM,無法訪問 window/document 對象,也無法使用 localStorage 等,如果使用這些 API,代碼將會報錯:

4

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)
  })
})();

執行結果:

5

可以明顯看到,性能耗時相差將近 6 倍,這數字會隨着對象屬性越多,耗時越長!!所以在循環中應當儘量避免讀取對象屬性,儘可能使用變量來做循環條件!!

寫在最後

可以使用 Web Worker 同時啓用多個工作線程,只是在任務調度的時候,需要注意響應結果的先後順序是否對主線程的運行有影響。

一些複雜的計算任務(比如視頻轉碼,圖片壓縮,圖片處理等),都丟給子線程處理吧,咱們前端也可以玩玩多線程~~

user avatar cyzf 頭像 smalike 頭像 linlinma 頭像 nihaojob 頭像 zourongle 頭像 leexiaohui1997 頭像 solvep 頭像 shuirong1997 頭像 woniuseo 頭像 Z-HarOld 頭像 wmbuke 頭像 youyoufei 頭像
點贊 78 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.