1. 程式人生 > 實用技巧 >淺析Web Worker

淺析Web Worker

  以前我們總說,JS是單執行緒沒有多執行緒,當JS在頁面中執行長耗時同步任務的時候就會導致頁面假死影響使用者體驗,從而需要設定把任務放在任務佇列中;執行任務佇列中的任務也並非多執行緒進行的,然而現在HTML5提供了我們前端開發這樣的能力 - Web Workers API,我們一起來看一看 Web Worker 是什麼,怎麼去使用它,在實際生產中如何去用它來進行產出。

一、概述

  Web Workers 使得一個Web應用程式可以在與主執行執行緒分離的後臺執行緒中執行一個指令碼操作。這樣做的好處是可以在一個單獨的執行緒中執行費時的處理任務,從而允許主(通常是UI)執行緒執行而不被阻塞

  它的作用就是給JS創造多執行緒執行環境

,允許主執行緒建立worker執行緒,分配任務給後者,主執行緒執行的同時worker執行緒也在執行,相互不干擾,在worker執行緒執行結束後把結果返回給主執行緒。這樣做的好處是主執行緒可以把計算密集型或高延遲的任務交給worker執行緒執行,這樣主執行緒就會變得輕鬆,不會被阻塞或拖慢。這並不意味著JS語言本身支援了多執行緒能力,而是瀏覽器作為宿主環境提供了JS一個多執行緒執行的環境

  不過因為worker一旦新建,就會一直執行,不會被主執行緒的活動打斷,這樣有利於隨時響應主執行緒的通性,但是也會造成資源的浪費,所以不應過度使用,用完注意關閉。或者說:如果worker無例項引用,該worker空閒後立即會被關閉;如果worker實列引用不為0,該worker空閒也不會被關閉。

二、使用

1、限制

  worker執行緒的使用有一些注意點:

(1)同源限制:worker執行緒執行的指令碼檔案必須和主執行緒的指令碼檔案同源,這是當然的了,總不能允許worker執行緒到別人電腦上到處讀檔案吧

(2)檔案限制:為了安全,worker執行緒無法讀取本地檔案,它所載入的指令碼必須來自網路,且需要與主執行緒的指令碼同源

(3)DOM操作限制:worker執行緒在與主執行緒的window不同的另一個全域性上下文中執行,其中無法讀取主執行緒所在網頁的DOM物件,也不能獲取 documentwindow等物件,但是可以獲取navigatorlocation(只讀)XMLHttpRequest

setTimeout等瀏覽器API。

(4)通訊限制:worker執行緒與主執行緒不在同一個上下文,不能直接通訊,需要通過postMessage方法來通訊。

(5)指令碼限制:worker執行緒不能執行alertconfirm,但可以使用 XMLHttpRequest 物件發出ajax請求。

2、例項

  在主執行緒中生成 Worker 執行緒很容易

var myWorker = new Worker(jsUrl, options)

  Worker()建構函式,第一個引數是指令碼的網址(必須遵守同源政策),該引數是必需的,且只能載入 JS 指令碼,否則報錯。第二個引數是配置物件,該物件可選。它的一個作用就是指定 Worker 的名稱,用來區分多個 Worker 執行緒

// 主執行緒
var myWorker = new Worker('worker.js', { name : 'myWorker' });

// Worker 執行緒
self.name // myWorker

  關於api什麼的,直接上例子大概就能明白了,首先是worker執行緒的js檔案:

// workerThread1.js
let i = 1

function simpleCount() {
  i++
  self.postMessage(i)
  setTimeout(simpleCount, 1000)
}

simpleCount()

self.onmessage = ev => {
  postMessage(ev.data + ' 呵呵~')
}

  在HTML檔案中的body中:

// 主執行緒,HTML檔案的body標籤中

<div>
  Worker 輸出內容:<span id='app'></span>
  <input type='text' title='' id='msg'>
  <button onclick='sendMessage()'>傳送</button>
  <button onclick='stopWorker()'>stop!</button>
</div>

<script type='text/javascript'>
  if (typeof(Worker) === 'undefined')    // 使用Worker前檢查一下瀏覽器是否支援
    document.writeln(' Sorry! No Web Worker support.. ')
  else {
    window.w = new Worker('workerThread1.js')
    window.w.onmessage = ev => {
      document.getElementById('app').innerHTML = ev.data
    }
    
    window.w.onerror = err => {
      w.terminate()
      console.log(error.filename, error.lineno, error.message) // 發生錯誤的檔名、行號、錯誤內容
    }
    
    function sendMessage() {
      const msg = document.getElementById('msg')
      window.w.postMessage(msg.value)
    }
    
    function stopWorker() {
      window.w.terminate()
    }
  }
</script>

  可以自己執行一下看看效果,上面用到了一些常用的api:

  主執行緒中的api,worker表示是 Worker 的例項:

  • worker.postMessage: 主執行緒往worker執行緒發訊息,訊息可以是任意型別資料,包括二進位制資料
  • worker.terminate: 主執行緒關閉worker執行緒
  • worker.onmessage: 指定worker執行緒發訊息時的回撥,也可以通過worker.addEventListener('message',cb)的方式
  • worker.onerror: 指定worker執行緒發生錯誤時的回撥,也可以 worker.addEventListener('error',cb)

  Worker執行緒中全域性物件為 self,代表子執行緒自身,這時 this指向self,其上有一些api:

  • self.postMessage: worker執行緒往主執行緒發訊息,訊息可以是任意型別資料,包括二進位制資料
  • self.close: worker執行緒關閉自己
  • self.onmessage: 指定主執行緒發worker執行緒訊息時的回撥,也可以self.addEventListener('message',cb)
  • self.onerror: 指定worker執行緒發生錯誤時的回撥,也可以 self.addEventListener('error',cb)

  注意,w.postMessage(aMessage, transferList) 方法接受兩個引數,aMessage 是可以傳遞任何型別資料的,包括物件,這種通訊是拷貝關係,即是傳值而不是傳址,Worker 對通訊內容的修改,不會影響到主執行緒。事實上,瀏覽器內部的執行機制是,先將通訊內容序列化,然後把序列化後的字串發給 Worker,後者再將它還原。一個可選的 Transferable 物件的陣列,用於傳遞所有權。如果一個物件的所有權被轉移,在傳送它的上下文中將變為不可用(中止),並且只有在它被髮送到的worker中可用。可轉移物件是如ArrayBuffer,MessagePort或ImageBitmap的例項物件,transferList陣列中不可傳入null。

  Transferable 介面代表一個能在不同可執行上下文之間,例如如主執行緒和 Worker 之間,相互傳遞的物件。這是一個抽象介面,沒有任何物件屬於此型別。它也沒有定義任何方法和屬性;它只是一個標籤,用來指示物件在特定場合下,比如如通過 Worker.postMessage() 方法傳遞到 Worker,是可用的。

備註:技術上,Transferable 介面已不復存在。但是,Transferable 物件的效用依舊存在,只是其實現被移到了更加底層的位置。(轉而通過WebIDL 拓展屬性 [Transferable] 實現)。

ArrayBufferMessagePortImageBitmap 實現了此介面。

  更詳細的API參見 MDN - WorkerGlobalScope

  worker執行緒中載入指令碼的api:

importScripts('script1.js')    // 載入單個指令碼
importScripts('script1.js', 'script2.js')    // 載入多個指令碼

三、實戰場景

  個人覺得,Web Worker我們可以當做計算器來用,需要用的時候掏出來摁一摁,不用的時候一定要收起來。

1、加密資料:有些加解密的演算法比較複雜,或者在加解密很多資料的時候,這會非常耗費計算資源,導致UI執行緒無響應,因此這是使用Web Worker的好時機,使用Worker執行緒可以讓使用者更加無縫的操作UI。

2、預取資料:有時候為了提升資料載入速度,可以提前使用Worker執行緒獲取資料,因為Worker執行緒是可以是用 XMLHttpRequest 的。

3、預渲染:在某些渲染場景下,比如渲染複雜的canvas的時候需要計算的效果比如反射、折射、光影、材料等,這些計算的邏輯可以使用Worker執行緒來執行,也可以使用多個Worker執行緒。

4、複雜資料處理場景:某些檢索、排序、過濾、分析會非常耗費時間,這時可以使用Web Worker來進行,不佔用主執行緒。

5、預載入圖片:有時候一個頁面有很多圖片,或者有幾個很大的圖片的時候,如果業務限制不考慮懶載入,也可以使用Web Worker來載入圖片,可以參考一下這篇文章的探索,這裡簡單提要一下。

// 主執行緒
let w = new Worker("js/workers.js");
w.onmessage = function (event) {
  var img = document.createElement("img");
  img.src = window.URL.createObjectURL(event.data);
  document.querySelector('#result').appendChild(img)
}
// worker執行緒
let arr = [...好多圖片路徑];
for (let i = 0, len = arr.length; i < len; i++) {
  let req = new XMLHttpRequest();
  req.open('GET', arr[i], true);
  req.responseType = "blob";
  req.setRequestHeader("client_type", "DESKTOP_WEB");
  req.onreadystatechange = () => {
    if (req.readyState == 4) {
      postMessage(req.response);
    }
  }
  req.send(null);
}

  在實戰的時候注意

  • 雖然使用worker執行緒不會佔用主執行緒,但是啟動worker會比較耗費資源
  • 主執行緒中使用XMLHttpRequest在請求過程中瀏覽器另開了一個非同步http請求執行緒,但是互動過程中還是要消耗主執行緒資源

  在 Webpack 專案裡面使用 Web Worker 請參照:怎麼在 ES6+Webpack 下使用 Web Worker