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