Web Worker
JavaScript 語言采用的是單線程模型,也就是說,所有任務只能在一個線程上完成,一次只能做一件事。前面的任務沒做完,后面的任務只能等著。隨著電腦計算能力的增強,尤其是多核 CPU 的出現,單線程帶來很大的不便,無法充分發揮計算機的計算能力。
Web Worker 的作用,就是為 JavaScript 創造多線程環境,允許主線程創建 Worker 線程,將一些任務分配給后者運行。在主線程運行的同時,Worker 線程在后臺運行,兩者互不干擾。等到 Worker 線程完成計算任務,再把結果返回給主線程。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 線程負擔了,主線程(通常負責 UI 交互)就會很流暢,不會被阻塞或拖慢。
Worker 線程一旦新建成功,就會始終運行,不會被主線程上的活動(比如用戶點擊按鈕、提交表單)打斷。這樣有利于隨時響應主線程的通信。但是,這也造成了 Worker 比較耗費資源,不應該過度使用,而且一旦使用完畢,就應該關閉。
- webWorker創建一個新線程,不會阻塞主線程從而渲染流暢(主線程阻塞會阻塞渲染)
- WebWorker不會被主線程打斷,但也造成Worker比較耗費資源,一旦使用完畢,就應該關閉
- 同源限制:分配給Worker線程運行的腳本文件,必須與主線程的腳本文件同源
- DOM限制:Worker線程沒法操作和讀取主線程所在的DOM對象,也無法使用document、window、parent這些對象,但是Worker線程可以訪問navigator對象和location對象
- Worker和主線程不能直接通信,需要通過消息完成
- 腳本限制:不能指向alrt()和confirm方法
- 文件限制:加載的腳本必須來自網絡,不能為本地文件
API
構造函數:
var myWorker = new Worker(jsUrl, options);
jsUrl是腳本的網址,必須遵循同源策略,并且只能加載JS腳本,否則會報錯。
第二個參數是配置對象:
// 主線程
var myWorker = new Worker('worker.js', { name : 'myWorker' });
// Worker 線程
self.name // myWorker
Worker線程對象的屬性和方法(主線程使用):
- Worker.onerror:指定 error 事件的監聽函數。
- Worker.onmessage:指定 message 事件的監聽函數,發送過來的數據在Event.data屬性中
- Worker.onmessageerror:指定 messageerror 事件的監聽函數。發送的數據無法序列化成字符串時,會觸發這個事件。
- Worker.postMessage():向 Worker 線程發送消息。
- Worker.terminate():立即終止 Worker 線程
Worker線程的屬性和方法(worker線程使用):
- self.name: Worker 的名字。該屬性只讀,由構造函數指定。
- self.onmessage:指定message事件的監聽函數。
- self.onmessageerror:指定 messageerror 事件的監聽函數。發送的數據無法序列化成字符串時,會觸發這個事件。
- self.close():關閉 Worker 線程。
- self.postMessage():向產生這個 Worker 線程發送消息。
- self.importScripts():加載 JS 腳本。
基本使用
構造函數:Worker(url),接收一個文件的url,獲取文件來創建一個Worker線程。
var worker = new Worker('work.js')
主線程通過worker.onmessage
來接收worker的數據,通過worker.postMessage()
來傳遞給worker數據。
worker.postMessage('hello')
worker.onmessage = function(event){
//event.data為傳過來的數據
//do something
}
self.onmessage = function(e){
console.log(e.data)
}
//或者是
self.addEventListener('message',function(e){
console.log(e.data)
})
通過importScripts方法可以在worker內部加載其他腳本
importScripts('file1','file2')
關閉worker:
//主線程中
worker.terminate()
//worker線程
self.close()
傳值問題
主線程與 Worker 之間的通信內容,可以是文本,也可以是對象。需要注意的是,這種通信是拷貝關系,即是傳值而不是傳址,Worker 對通信內容的修改,不會影響到主線程。事實上,瀏覽器內部的運行機制是,先將通信內容串行化,然后把串行化后的字符串發給 Worker,后者再將它還原。
主線程和Worker之間傳遞的數據是傳值的,而不是傳址的,這樣避免了Worker線程操作主線程的數據,傳遞數據時,會調用內容的toString()方法(二進制數據除外),再傳值,所以對象盡量經過JSON.stringify后傳值
主線程與worker之間可以交換二進制數據,比如File、Blob、ArrayBuffer等類型,也可以在線程之間發送.
// 主線程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);
// Worker 線程
self.onmessage = function (e) {
var uInt8Array = e.data;
postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};
轉移大文件而不是拷貝
拷貝方式發送二進制數據,會造成性能問題。比如,主線程向 Worker 發送一個 500MB 文件,默認情況下瀏覽器會生成一個原文件的拷貝。為了解決這個問題,JavaScript 允許主線程把二進制數據直接轉移給子線程,但是一旦轉移,主線程就無法再使用這些二進制數據了,這是為了防止出現多個線程同時修改數據的麻煩局面。這種轉移數據的方法,叫做Transferable Objects。這使得主線程可以快速把數據交給 Worker,對于影像處理、聲音處理、3D 運算等就非常方便了,不會產生性能負擔
// Transferable Objects 格式
worker.postMessage(aMessage, transferList);
// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
- transferList:數組,用于傳遞所有權,如果一個對象的所有權被轉義,在發送它的上下文中將變為不可用,可轉義對象必須是ArrayBuffer、MessagePort(MessageChanel的port)或者ImageBitmap的實例。
在頁面內創建Web Worker
通常情況下,Worker 載入的是一個單獨的 JavaScript 腳本文件,但是也可以載入與主線程在同一個網頁的代碼。
通過script標簽
<body>
<script id="worker" type="app/worker">
addEventListener('message', function () {
postMessage('some message');
}, false);
</script>
</body>
上面是一段嵌入網頁的腳本,注意必須指定<script>標簽的type屬性是一個瀏覽器不認識的值,上例是app / worker。
//基于script的內容創建一個Blob對象
var blob = new Blob([document.querySelector('#worker').textContent]);
// 生成指向Blob對象的blobURL
var url = window.URL.createObjectURL(blob);
// 請求blobURL來創建worker
var worker = new Worker(url);
worker.onmessage = function (e) {
// e.data === 'some message'
};
worker線程完成輪詢
有時,瀏覽器需要輪詢服務器狀態,以便第一時間得知狀態改變。這個工作可以放在 Worker 里面。
function createWorker(f) {
var blob = new Blob(['(' + f.toString() +')()']);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
return worker;
}
var pollingWorker = createWorker(function (e) {
var cache;
function compare(new, old) { ... };
setInterval(function () {
fetch('/my-api-endpoint').then(function (res) {
var data = res.json();
if (!compare(data, cache)) {
cache = data;
self.postMessage(data);
}
})
}, 1000)
});
pollingWorker.onmessage = function () {
// render data
}
pollingWorker.postMessage('init');
一個完整的使用例子:
function createWorker(fn) {
var blob = new Blob([`(${fn.toString()})()`])
var blobURL = window.URL.createObjectURL(blob)
var worker = new Worker(blobURL)
return worker
}
var webWorker = createWorker(function () {
function init() {
let i = 0
setInterval(function () {
self.postMessage(++i)
}, 1000)
}
self.onmessage = function (messageEvent) {
switch (messageEvent.data) {
case "init":
init()
break
case "stop":
console.log("close!");
self.close();
break;
}
}
})
webWorker.onmessage = function (event) {
console.log(event.data)
if (event.data === 5) {
webWorker.postMessage("stop")
//主線程關閉webWorker.terminate()
}
}
webWorker.postMessage("init")
MessageChannel
Vue的$nextTick實現中,優先檢測是否支持原生的setImmediate(Node和高版本IE\chrome支持),不支持的話檢測是否支持原生的MessageChannel,如果再不支持就會降級為setTimeout.
setImmediate\MessageChannel\setTimeout都屬于宏任務,宏任務就是等待主線程空閑后才會被執行的任務(宏任務->微任務->渲染->宏任務的執行過程),實現宏任務效果最理想的就是setImmediate,MessageChannel也可以替代,但setImmediate和MessageChannel的瀏覽器支持沒有setTimeout好,setTimeout有一個致命缺點就是即使設置setTimeout(fn,0),fn不會立即被推入到宏任務隊列中,在chrome中至少是4ms以上。而 setImmediate可以將任務直接推入到宏任務隊列
MessageChannle允許我們創建一個新的消息通道,并通過它的兩個MessagePort屬性發送數據。
var channel = new MessageChannel()
//兩個端口
channel.port1
channel.port2
MessageChannel創建了一個通信的管道,這個管道有兩個端口,每個端口都可以通過postMessage發送數據,而一個端口只要綁定了onmessage回調方法,就可以接收從另一個端口傳過來的數據。(這一點和Web Worker類似,傳遞的數據從event.data獲取)
var channel = new MessageChannel();
var port1 = channel.port1;
var port2 = channel.port2;
port1.onmessage = function(event) {
console.log("port1收到來自port2的數據:" + event.data);
}
port2.onmessage = function(event) {
console.log("port2收到來自port1的數據:" + event.data);
}
port1.postMessage("發送給port2");
port2.postMessage("發送給port1");
多個worker之間的通信
當我們使用多個Web Worker并想要在兩個Web Worker之間實現通信的時候,MessageChannel可以派上用場:
//主線程
var w1 = new Worker("worker1.js");
var w2 = new Worker("worker2.js");
var ch = new MessageChannel();
//由于直接傳遞會被轉化成字符串,這里使用轉移數據的方法
//MessageChannel會被自動存儲在event.ports里面
w1.postMessage("port1", [ch.port1]);
w2.postMessage("port2", [ch.port2]);
w2.onmessage = function(e) {
console.log(e.data);
}
//worker1
self.onmessage = function(e) {
const port = e.ports[0];
//傳遞給worker2
port.postMessage("this is from worker1")
}
//worker2
onmessage = function(e) {
const port = e.ports[0];
//接受worker1消息
port.onmessage = function(e){
//傳遞回主線程
self.postMessage(e.data)
}
}