簡介
以前我們總說,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 空閑也不會被關閉。
兼容性
注意事項
worker 線程的使用有一些注意點:
- 同源限制 worker 線程執行的腳本文件必須和主線程的腳本文件同源,這是當然的了,總不能允許 worker 線程到別人電腦上到處讀文件吧
- 文件限制 為了安全,worker 線程無法讀取本地文件,它所加載的腳本必須來自網絡,且需要與主線程的腳本同源
- DOM 操作限制 worker 線程在與主線程的 window 不同的另一個全局上下文中運行,其中無法讀取主線程所在網頁的 DOM 對象,也不能獲取 document、window等對象,但是可以獲取navigator、location(只讀)、XMLHttpRequest、setTimeout族等瀏覽器API
- 通信限制 worker 線程與主線程不在同一個上下文,不能直接通信,需要通過postMessage方法來通信
腳本限制 worker 線程不能執行alert、confirm,但可以使用 XMLHttpRequest 對象發出 ajax 請求
示例
在主線程中生成 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 文件中
關于 api 什么的,直接上例子大概就能明白了,首先是 worker 線程的 js 文件:// workerThread1.jslet i = 1function simpleCount() {i++self.postMessage(i)setTimeout(simpleCount, 1000)
}simpleCount()self.onmessage = ev => {postMessage(ev.data + ' 呵呵~')
}
在 HTML 文件中的 body 中:
在 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。
worker 線程中加載腳本的 api:
importScripts('script1.js') // 加載單個腳本
importScripts('script1.js', 'script2.js') // 加載多個腳本
使用場景
個人覺得,Web Worker 我們可以當做計算器來用,需要用的時候掏出來摁一摁,不用的時候一定要收起來。
加密數據 有些加解密的算法比較復雜,或者在加解密很多數據的時候,這會非常耗費計算資源,導致 UI 線程無響應,因此這是使用 Web Worker 的好時機,使用 Worker 線程可以讓用戶更加無縫的操作 UI。
預取數據 有時候為了提升數據加載速度,可以提前使用 Worker 線程獲取數據,因為 Worker 線程是可以是用 XMLHttpRequest 的。
預渲染 在某些渲染場景下,比如渲染復雜的 canvas 的時候需要計算的效果比如反射、折射、光影、材料等,這些計算的邏輯可以使用 Worker 線程來執行,也可以使用多個 Worker 線。
復雜數據處理場景 某些檢索、排序、過濾、分析會非常耗費時間,這時可以使用 Web Worker 來進行,不占用主線程。
預加載圖片 有時候一個頁面有很多圖片,或者有幾個很大的圖片的時候,如果業務限制不考慮懶加載,也可以使用 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
參考文章
mdn
前端 Web Workers 到底是什么
Web Worker在項目中的妙用