概述
JavaScript 語言采用的是單線程模型,也就是說,所有任務(wù)只能在一個線程上完成,一次只能做一件事。前面的任務(wù)沒做完,后面的任務(wù)只能等著。隨著電腦計算能力的增強,尤其是多核 CPU 的出現(xiàn),單線程帶來很大的不便,無法充分發(fā)揮計算機的計算能力。
Web Worker 的作用,就是為 JavaScript 創(chuàng)造多線程環(huán)境,允許主線程創(chuàng)建 Worker 線程,將一些任務(wù)分配給后者運行。在主線程運行的同時,Worker 線程在后臺運行,兩者互不干擾。等到 Worker 線程完成計算任務(wù),再把結(jié)果返回給主線程。這樣的好處是,一些計算密集型或高延遲的任務(wù)可以交由 Worker 線程執(zhí)行,主線程(通常負責 UI 交互)能夠保持流暢,不會被阻塞或拖慢。
Worker 線程一旦新建成功,就會始終運行,不會被主線程上的活動(比如用戶點擊按鈕、提交表單)打斷。這樣有利于隨時響應(yīng)主線程的通信。但是,這也造成了 Worker 比較耗費資源,不應(yīng)該過度使用,而且一旦使用完畢,就應(yīng)該關(guān)閉。
Web Worker 有以下幾個使用注意點。
(1)同源限制
分配給 Worker 線程運行的腳本文件,必須與主線程的腳本文件同源。
(2)DOM 限制
Worker 線程所在的全局對象,與主線程不一樣,無法讀取主線程所在網(wǎng)頁的 DOM 對象,也無法使用document
、window
、parent
這些對象。但是,Worker 線程可以使用navigator
對象和location
對象。
(3)全局對象限制
Worker 的全局對象WorkerGlobalScope
,不同于網(wǎng)頁的全局對象Window
,很多接口拿不到。比如,理論上 Worker 線程不能使用console.log
,因為標準里面沒有提到 Worker 的全局對象存在console
接口,只定義了Navigator
接口和Location
接口。不過,瀏覽器實際上支持 Worker 線程使用console.log
,保險的做法還是不使用這個方法。
(4)通信聯(lián)系
Worker 線程和主線程不在同一個上下文環(huán)境,它們不能直接通信,必須通過消息完成。
(5)腳本限制
Worker 線程不能執(zhí)行alert()
方法和confirm()
方法,但可以使用 XMLHttpRequest 對象發(fā)出 AJAX 請求。
(6)文件限制
Worker 線程無法讀取本地文件,即不能打開本機的文件系統(tǒng)(file://
),它所加載的腳本,必須來自網(wǎng)絡(luò)。
基本用法
主線程
主線程采用new
命令,調(diào)用Worker()
構(gòu)造函數(shù),新建一個 Worker 線程。
var worker = new Worker('work.js');
Worker()
構(gòu)造函數(shù)的參數(shù)是一個腳本文件,該文件就是 Worker 線程所要執(zhí)行的任務(wù)。由于 Worker 不能讀取本地文件,所以這個腳本必須來自網(wǎng)絡(luò)。如果下載沒有成功(比如404錯誤),Worker 就會默默地失敗。
然后,主線程調(diào)用worker.postMessage()
方法,向 Worker 發(fā)消息。
worker.postMessage('Hello World');
worker.postMessage({method: 'echo', args: ['Work']});
worker.postMessage()
方法的參數(shù),就是主線程傳給 Worker 的數(shù)據(jù)。它可以是各種數(shù)據(jù)類型,包括二進制數(shù)據(jù)。
接著,主線程通過worker.onmessage
指定監(jiān)聽函數(shù),接收子線程發(fā)回來的消息。
worker.onmessage = function (event) {
doSomething(event.data);
}
function doSomething() {
// 執(zhí)行任務(wù)
worker.postMessage('Work done!');
}
上面代碼中,事件對象的data
屬性可以獲取 Worker 發(fā)來的數(shù)據(jù)。
Worker 完成任務(wù)以后,主線程就可以把它關(guān)掉。
worker.terminate();
Worker 線程
Worker 線程內(nèi)部需要有一個監(jiān)聽函數(shù),監(jiān)聽message
事件。
self.addEventListener('message', function (e) {
self.postMessage('You said: ' + e.data);
}, false);
上面代碼中,self
代表子線程自身,即子線程的全局對象。因此,等同于下面兩種寫法。
// 寫法一
this.addEventListener('message', function (e) {
this.postMessage('You said: ' + e.data);
}, false);
// 寫法二
addEventListener('message', function (e) {
postMessage('You said: ' + e.data);
}, false);
除了使用self.addEventListener()
指定監(jiān)聽函數(shù),也可以使用self.onmessage
指定。監(jiān)聽函數(shù)的參數(shù)是一個事件對象,它的data
屬性包含主線程發(fā)來的數(shù)據(jù)。self.postMessage()
方法用來向主線程發(fā)送消息。
根據(jù)主線程發(fā)來的數(shù)據(jù),Worker 線程可以調(diào)用不同的方法,下面是一個例子。
self.addEventListener('message', function (e) {
var data = e.data;
switch (data.cmd) {
case 'start':
self.postMessage('WORKER STARTED: ' + data.msg);
break;
case 'stop':
self.postMessage('WORKER STOPPED: ' + data.msg);
self.close(); // Terminates the worker.
break;
default:
self.postMessage('Unknown command: ' + data.msg);
};
}, false);
上面代碼中,self.close()
用于在 Worker 內(nèi)部關(guān)閉自身。
Worker 加載腳本
Worker 內(nèi)部如果要加載其他腳本,有一個專門的方法importScripts()
。
importScripts('script1.js');
該方法可以同時加載多個腳本。
importScripts('script1.js', 'script2.js');
錯誤處理
主線程可以監(jiān)聽 Worker 是否發(fā)生錯誤。如果發(fā)生錯誤,Worker 會觸發(fā)主線程的error
事件。
worker.onerror = function (event) {
console.log(
'ERROR: Line ', event.lineno, ' in ', event.filename, ': ', event.message
);
};
// 或者
worker.addEventListener('error', function (event) {
// ...
});
Worker 內(nèi)部也可以監(jiān)聽error
事件。
關(guān)閉 Worker
使用完畢,為了節(jié)省系統(tǒng)資源,必須關(guān)閉 Worker。
// 主線程
worker.terminate();
// Worker 線程
self.close();
數(shù)據(jù)通信
前面說過,主線程與 Worker 之間的通信內(nèi)容,可以是文本,也可以是對象。需要注意的是,這種通信是拷貝關(guān)系,即是傳值而不是傳址,Worker 對通信內(nèi)容的修改,不會影響到主線程。事實上,瀏覽器內(nèi)部的運行機制是,先將通信內(nèi)容串行化,然后把串行化后的字符串發(fā)給 Worker,后者再將它還原。
主線程與 Worker 之間也可以交換二進制數(shù)據(jù),比如 File、Blob、ArrayBuffer 等類型,也可以在線程之間發(fā)送。下面是一個例子。
// 主線程
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);
};
但是,拷貝方式發(fā)送二進制數(shù)據(jù),會造成性能問題。比如,主線程向 Worker 發(fā)送一個 500MB 文件,默認情況下瀏覽器會生成一個原文件的拷貝。為了解決這個問題,JavaScript 允許主線程把二進制數(shù)據(jù)直接轉(zhuǎn)移給子線程,但是一旦轉(zhuǎn)移,主線程就無法再使用這些二進制數(shù)據(jù)了,這是為了防止出現(xiàn)多個線程同時修改數(shù)據(jù)的麻煩局面。這種轉(zhuǎn)移數(shù)據(jù)的方法,叫做Transferable Objects。這使得主線程可以快速把數(shù)據(jù)交給 Worker,對于影像處理、聲音處理、3D 運算等就非常方便了,不會產(chǎn)生性能負擔。
如果要直接轉(zhuǎn)移數(shù)據(jù)的控制權(quán),就要使用下面的寫法。
// Transferable Objects 格式
worker.postMessage(arrayBuffer, [arrayBuffer]);
// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);
同頁面的 Web Worker
通常情況下,Worker 載入的是一個單獨的 JavaScript 腳本文件,但是也可以載入與主線程在同一個網(wǎng)頁的代碼。
<!DOCTYPE html>
<body>
<script id="worker" type="app/worker">
addEventListener('message', function () {
postMessage('some message');
}, false);
</script>
</body>
</html>
上面是一段嵌入網(wǎng)頁的腳本,注意必須指定<script>
標簽的type
屬性是一個瀏覽器不認識的值,上例是app/worker
。
然后,讀取這一段嵌入頁面的腳本,用 Worker 來處理。
var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);
worker.onmessage = function (e) {
// e.data === 'some message'
};
上面代碼中,先將嵌入網(wǎng)頁的腳本代碼,轉(zhuǎn)成一個二進制對象,然后為這個二進制對象生成 URL,再讓 Worker 加載這個 URL。這樣就做到了,主線程和 Worker 的代碼都在同一個網(wǎng)頁上面。
實例:Worker 線程完成輪詢
有時,瀏覽器需要輪詢服務(wù)器狀態(tài),以便第一時間得知狀態(tài)改變。這個工作可以放在 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');
上面代碼中,Worker 每秒鐘輪詢一次數(shù)據(jù),然后跟緩存做比較。如果不一致,就說明服務(wù)端有了新的變化,因此就要通知主線程。
實例: Worker 新建 Worker
Worker 線程內(nèi)部還能再新建 Worker 線程(目前只有 Firefox 瀏覽器支持)。下面的例子是將一個計算密集的任務(wù),分配到10個 Worker。
主線程代碼如下。
var worker = new Worker('worker.js');
worker.onmessage = function (event) {
document.getElementById('result').textContent = event.data;
};
Worker 線程代碼如下。
// worker.js
// settings
var num_workers = 10;
var items_per_worker = 1000000;
// start the workers
var result = 0;
var pending_workers = num_workers;
for (var i = 0; i < num_workers; i += 1) {
var worker = new Worker('core.js');
worker.postMessage(i * items_per_worker);
worker.postMessage((i + 1) * items_per_worker);
worker.onmessage = storeResult;
}
// handle the results
function storeResult(event) {
result += event.data;
pending_workers -= 1;
if (pending_workers <= 0)
postMessage(result); // finished!
}
上面代碼中,Worker 線程內(nèi)部新建了10個 Worker 線程,并且依次向這10個 Worker 發(fā)送消息,告知了計算的起點和終點。計算任務(wù)腳本的代碼如下。
// core.js
var start;
onmessage = getStart;
function getStart(event) {
start = event.data;
onmessage = getEnd;
}
var end;
function getEnd(event) {
end = event.data;
onmessage = null;
work();
}
function work() {
var result = 0;
for (var i = start; i < end; i += 1) {
// perform some complex calculation here
result += 1;
}
postMessage(result);
close();
}
API
主線程
瀏覽器原生提供Worker()
構(gòu)造函數(shù),用來供主線程生成 Worker 線程。
var myWorker = new Worker(jsUrl, options);
Worker()
構(gòu)造函數(shù),可以接受兩個參數(shù)。第一個參數(shù)是腳本的網(wǎng)址(必須遵守同源政策),該參數(shù)是必需的,且只能加載 JS 腳本,否則會報錯。第二個參數(shù)是配置對象,該對象可選。它的一個作用就是指定 Worker 的名稱,用來區(qū)分多個 Worker 線程。
// 主線程
var myWorker = new Worker('worker.js', { name : 'myWorker' });
// Worker 線程
self.name // myWorker
Worker()
構(gòu)造函數(shù)返回一個 Worker 線程對象,用來供主線程操作 Worker。Worker 線程對象的屬性和方法如下。
- Worker.onerror:指定 error 事件的監(jiān)聽函數(shù)。
- Worker.onmessage:指定 message 事件的監(jiān)聽函數(shù),發(fā)送過來的數(shù)據(jù)在
Event.data
屬性中。 - Worker.onmessageerror:指定 messageerror 事件的監(jiān)聽函數(shù)。發(fā)送的數(shù)據(jù)無法序列化成字符串時,會觸發(fā)這個事件。
- Worker.postMessage():向 Worker 線程發(fā)送消息。
- Worker.terminate():立即終止 Worker 線程。
Worker 線程
Web Worker 有自己的全局對象,不是主線程的window
,而是一個專門為 Worker 定制的全局對象。因此定義在window
上面的對象和方法不是全部都可以使用。
Worker 線程有一些自己的全局屬性和方法。
- self.name: Worker 的名字。該屬性只讀,由構(gòu)造函數(shù)指定。
- self.onmessage:指定
message
事件的監(jiān)聽函數(shù)。 - self.onmessageerror:指定 messageerror 事件的監(jiān)聽函數(shù)。發(fā)送的數(shù)據(jù)無法序列化成字符串時,會觸發(fā)這個事件。
- self.close():關(guān)閉 Worker 線程。
- self.postMessage():向產(chǎn)生這個 Worker 的線程發(fā)送消息。
- self.importScripts():加載 JS 腳本。
(完)
更多建議: