JavaScript是單線程的,但Web Workers規範允許Web應用程式可以在獨立于主線程的背景線程中,建立子線程,運作一個腳本操作,進而可以在獨立的線程中執行費時的處理任務,進而不會阻塞主線程(通常是UI線程),Worker線程計算結束後,再把結果傳回給主線程,這樣就解決了Javascript運作時會幹擾使用者界面的問題;
例如:
<div id="result"></div>
<script>
var result = document.getElementById("result");
var worker = new Worker("worker.js");
worker.onmessage = function(event){
result.innerText = event.data;
};
</script>
worker.js:
var num = 0;
setInterval(function(){
postMessage(num++);
},1000);
浏覽器實作Web Workers的方式有多種,可以使用線程、背景程序或者運作在其他處理器核心上的程序等等;Web Workers起初是作為HTML5标準的一部分,但後來獨立成一個相近的标準;IE9及以下不支援Workers。
應用場景:
Web Workers的實作為Web應用帶來了背景計算的能力,它可以将一些耗時的資料處理操作從主線程中剝離,進而極大減輕了因計算量大造成的UI阻塞而出現的界面渲染卡、掉幀的情況,使主線程更加專注于頁面渲染和互動,更大程度地利用了終端硬體的性能;
其應用的場景:數學運算、大資料處理、懶加載、文本分析、流媒體資料處理、canvas圖形繪制、圖像處理等等。
專用Worker和共享Worker:
worker有兩大類,一個是專用Worker(dedicated worker),是專門為某個特定的頁面服務的;二是共享Worker(shared worker),這種worker可以在浏覽器的多個标簽中打開的同一個頁面間共享。
專用Worker:
隻能由生成它的腳本所使用;
一個Worker包含兩部分,一部分是Worker對象,該對象用于建立worker線程;第二部分是DedicatedWorkerGlobalScope,這是一個用來表示新建立的Worker内部的全局對象,也就是Worker線程内部使用的上下文對象;
使用Worker(URL[, options])構造函數執行個體化Worker對象,該對象執行指定的URL腳本;
worker.js線程:
console.log("my is worker");
主線程:
var worker = new Worker("worker.js");
console.log(worker); // Worder
參數URL指定的JavaScript檔案,包含了将運作獨立于主線程的worker線程中運作的代碼;
參數URL指定的worker JS檔案必須遵守同源政策,如果該URL是一個無效的URL,或者違反同源政策,将抛出SECURITY_ERR異常;如果此URL的MIME類型不是text/javascript類型,将抛出NetworkError異常;如果URL無法解析,将引發SyntaxError異常;
參數options是可選的配置對象,可用屬性如下:
- type:指定worker類型的字元串,可能的值是classic或module,如果未指定,将使用預設值classic;
- credentials:用以指定worker憑證的字元串,可能的值是omit(不要求憑證)、same-origin或include,如果未指定,或者type是classic,将使用預設值omit;
- name:用于指定Worker的名稱,用來區分多個Worker線程,主要用于調試目的;
如主線程:
var worker = new Worker("worker.js", {
type:"classic",
credentials:"omit",
name: "myWorker"
});
console.log(worker);
雖然可以在參數中指定options,但該參數的值隻能在Worker線程中才能通路;如,Worker線程:
postMessage({
name: self.name,
type: self.type,
credential: self.credential
});
主線程:
worker.onmessage = function(event){
console.log(event.data);
};
worker線程和主線程之間的資料傳遞,是通過這樣的消息機制進行的:雙方都使用postMessage()方法發送各自的消息,再使用onmessage事件處理函數來響應消息,且消息被包含在onmessage事件的data屬性中;
當頁面在Worker對象上調用postMessage()時,資料會以異步方式被傳遞給worker線程,進而觸發worker線程中的message事件;
它們的通信是雙向的,反過來也是一樣;
使用Worker對象的postMessage(message[, transferList])方法向worker傳遞消息,參數message為要發送的消息資料,參數transferList是可選的,它是一個Transferable對象的數組,用于轉移所有權;如主線程:
worker.postMessage("wangwei");
Worker線程是通過onmessage和onerror事件來接收資料和處理錯誤的,如:
onmessage = function(event){
console.log(event); // MessageEvent
console.log(event.data);
}
onerror = function(event){
console.log(event);
}
該事件類型是MessageEvent類型;
worker線程向主線程傳遞消息:
與主線程向worker線程傳遞消息一樣,隻不過由worker線程通過postMesage()發送消息,主線程通過onmessage和onerror事件接收鼠資料,如,主線程:
worker.onmessage = function(event){
console.log("Main:" + event.data);
};
worker.onerror = function(event){
console.log(event);
}
worker線程:
postMessage("我是worker線程");
參數message可以是任何能夠被序列化的值,也可以是對象,如:
worker.postMessage({
type: "command",
message: "start"
});
該方法一次隻能發送一個值或對象,是以,如果想傳遞多個值,可以使用數組,如,主線程:
worker.postMessage(["wangwei",18]);
worker線程:
onmessage = function (event) {
console.log(event.data);
}
Worker如果不能完成給定的任務時會觸發error事件;如:
worker.onerror = function(event){
console.log(event); // // ErrorEvent
console.log("Error: " + event.filename + " (" + event.lineno + "): " + event.message);
};
建議在使用Worker時,始終都要使用onerror事件,因為,不注冊onerror事件,Worker會在發生錯誤的時候,靜默失敗;
在message事件中,還可以使用postMessage()方法回送資料;如,主線程:
var worker = new Worker("worker.js");
worker.postMessage("Main向Worker發送的消息");
worker.onmessage = function(event){
console.log("Main: " + event.data);
}
worker.onerror = function(event){
console.log(event);
}
worker.js線程:
onmessage = function(event){
console.log("worker: " + event.data);
postMessage("Worker向Main回發的消息");
}
示例:數字排序,主線程:
var data = [18,45,11,98,3,6,768];
worker.postMessage(data);
worker.onmessage = function(event){
console.log(event.data);
};
worker線程:
self.onmessage = function(event){
var data = event.data;
data.sort(function(a,b){
return a - b;
});
self.postMessage(data);
};
示例:計算兩個數的乘積:
<style>
html{background-color: #7D2663;}
p{margin: 0;}
.controls {
width: 75%; margin: 10vw auto; padding: 4vw;
background-color: rgba(255,255,255,0.7);
border: 5px solid black;
opacity: 0.3; transition: 1s all;
}
.controls:hover, .controls:focus {opacity: 1;}
.controls label, .controls p, .controls input {font-size: 3vw;}
.controls div {padding-bottom: 1rem;}
</style>
<div class="controls">
<div>
<label for="number1">乘數1: </label>
<input type="text" id="number1" value="0">
</div>
<div>
<label for="number2">乘數2: </label>
<input type="text" id="number2" value="0">
</div>
<p class="result">結果: 0</p>
</div>
<script>
var first = document.querySelector('#number1');
var second = document.querySelector('#number2');
var result = document.querySelector('.result');
if (window.Worker) {
var myWorker = new Worker("worker.js");
first.onchange = function() {
myWorker.postMessage([first.value, second.value]);
console.log('向worker中發送第一個乘數');
}
second.onchange = function() {
myWorker.postMessage([first.value, second.value]);
console.log('向worker中發送第二個乘數');
}
myWorker.onmessage = function(event) {
result.textContent = event.data;
console.log('從worker接收到計算結果:' + event.data);
}
}else{
result.textContent = '浏覽器不支援 web workers';
}
</script>
worker.js:
onmessage = function(e) {
console.log('Worker: 從主線程接收到乘數');
var result = e.data[0] * e.data[1];
if(isNaN(result)) {
postMessage('請輸入兩個數字');
}else{
var workerResult = '結果: ' + result;
console.log('Worker: 已計算出結果,并回顯');
postMessage(workerResult);
}
}
終止Worker:
worker線程一旦建立成功,就會始終運作,不會被主線程上的活動打斷;這樣有利于随時響應主線程的通信,但是,這也造成了Worker比較耗費資源的問題,是以不應該過度使用worder,而且一旦使用完畢,就應該終止;
在任何時候,隻要調用terminate()方法就可以終止Worker的工作;此時,worker線程中的代碼會立即停止執行,後續的所有過程都不會再發生,包括error和message事件也不會再被觸發,如:
worker.terminate();
主線程:
<div id="result">等待接收消息...</div>
<button id="btnStart">開始</button>
<button id="btnClose">終止</button>
<script>
var worker = new Worker("worker.js");
var result = document.getElementById("result");
worker.onmessage = function(event){
result.innerHTML += "<br/>" + event.data;
}
worker.onerror = function(event){
console.log(event);
}
var btnStart = document.getElementById("btnStart");
btnStart.onclick = function(){
worker.postMessage("start");
};
var btnClose = document.getElementById("btnClose");
btnClose.onclick = function(){
worker.terminate();
result.innerHTML += "<br/>Worder Stop!";
};
</script>
worker線程:
var count = 0;
self.onmessage = function (event) {
self.postMessage('Worker Start: ');
setInterval(function(){
this.postMessage("我是模拟的資料,不要當真" + count++);
},2000);
};
可以根據主線程發來的資料,worker線程可以調用不同的方法,如:主線程:
<div id="result">等待接收消息...</div>
<button id="btnStart">開始</button>
<button id="btnClose">終止</button>
<script>
var result = document.getElementById("result");
var worker = new Worker("worker.js");
worker.postMessage({
cmd: "",
msg: "已連接配接..."
});
worker.onmessage = function(event){
result.innerHTML += "<br/>" + event.data;
}
worker.onerror = function(event){
console.log(event);
}
var btnStart = document.getElementById("btnStart");
btnStart.onclick = function(){
worker.postMessage({
cmd: "start",
msg: "開始做任務"
});
};
var btnClose = document.getElementById("btnClose");
btnClose.onclick = function(){
worker.postMessage({
cmd: "stop",
msg: "結束任務"
});
};
</script>
worker線程:
var count = 0;
self.onmessage = function (event) {
var data = event.data;
switch (data.cmd) {
case 'start':
self.postMessage('Worker Start: ' + data.msg);
setInterval(function(){
this.postMessage("我是模拟的資料,不要當真" + count++);
},2000);
break;
case 'stop':
self.postMessage('Worker Stop: ' + data.msg);
self.close();
break;
default:
self.postMessage(data.msg);
};
};
DedicatedWorkerGlobalScope(Worker線程):
關于Worker,最重要的是要知道它所執行的JavaScript代碼完全在另一個作用域(上下文)中,即DedicatedWorkerGlobalScope,其代表了專用worker的上下文,與目前主線程頁面中的代碼不共享作用域,也就是不同于目前的window;
其繼承自WorkerGlobalScope類;該類是DedicatedWorkerGlobalScope、SharedWorkerGlobalScope和ServiceWorkerGlobalScope類的父類;
worker線程:
console.log(self); // DedicatedWorkerGlobalScope
在worker内部中,this、self指向DedicatedWorkerGlobalScope這個上下文,在使用時也可以省略,如:
// 使用this
this.onmessage = function(event){
console.log(event);
}
// 使用self
self.onerror = function(event){
console.log(event);
}
// 省略
postMessage("我是worker線程");
為便于處理資料,Web Workers本身是一個最小化的運作環境:
最小化的navigator對象,包括onLine、appName、appVersion、userAgent和platform屬性,在Worker線程中被稱為WorkerNavigator對象;
隻讀的location對象,在Worder線程中是WorkerLocation對象;
還可以使用window對象的部分API ,如:setTimout()、setInterval()、clearTimeout()和clearInterval()方法、XMLHttpRequest,也包括WebSockets、IndexedDB等;
顯然,Web Workers的運作環境與頁面環境相比,功能是相當有限的;并且在Worker中的代碼不能通路DOM,也不能通過任何方式影響頁面的外觀,隻能使用少部分的window的功能;
如果想操作DOM,隻能間接地實作,即通過postMessage發送消息給主線程,然後在主線程那裡執行DOM操作;
主線程操作DOM:主線程:
worker.onmessage = function(event){
var person = event.data;
var div = document.createElement("div");
div.innerHTML = "姓名:" + person.name + "<br/>" +
"性别:" + person.sex + "<br/>" +
"年齡:" + person.age;
document.body.appendChild(div);
}
worker線程:
postMessage({
name: "wangwei",
sex: true,
age: 18
});
在worker線程内部,調用close()方法也可以停止工作,就像在主線程中調用terminate()方法一樣,Worker停止工作後就不會再有事件觸發了,如:
self.close();
示例:計算斐波那契數,fibonacci.js:
var results = [];
function resultReceiver(event){
results.push(parseInt(event.data));
if(results.length == 2)
postMessage(results[0] + results[1]);
}
function errorReceiver(event){
throw event.data;
}
onmessage = function(event){
var n = parseInt(event.data);
if(n == 0 || n == 1){
postMessage(n);
return;
}
for(var i=1; i<=2; i++){
var worker = new Worker("fibonacci.js");
worker.onmessage = resultReceiver;
worker.onerror = errorReceiver;
worker.postMessage(n - i);
}
};
首頁面:
<div id="result"></div>
<script>
var worker = new Worker("fibonacci.js");
worker.onmessage = function(event){
document.getElementById("result").textContent = event.data;
console.log("Got: " + event.data);
};
worker.onerror = function(error){
console.log("Worker error: " + error.message);
throw error;
};
worker.postMessage("8");
</script>
生成subworker:
如果需要的話,Worker也可以生成多個Worker,這就是所謂的subworker; subworker解析URI時會相對于父worker的位址而不是首頁面的位址;主線程:
var worker = new Worker("worker.js");
worker.onmessage = function(event){
console.log(event.data);
};
worker.js:
var subWorker = new Worker("subworker.js");
subWorker.onmessage = function(event){
event.data.age = 18;
postMessage(event.data);
};
subworker.js:
postMessage({
name: "wangwei",
message: "我是subworker"
})
subworker的error事件名為onmessageerror;
在多核cpu,且要進行并行計算的情況下,使用subworker特别有用;
示例:使用多個subworker并行計算主線程:
var worker = new Worker('worker.js');
worker.onmessage = function (event) {
document.getElementById('result').textContent = event.data;
};
worker線程:
var num_workers = 10;
var items_per_worker = 100;
var result = 0;
var pending_workers = num_workers;
for(var i = 0; i < num_workers; i++) {
var worker = new Worker('core.js');
worker.postMessage(i * items_per_worker);
worker.postMessage((i + 1) * items_per_worker);
worker.onmessage = messageHandler;
}
function messageHandler(event){
result += event.data;
pending_workers -= 1;
if(pending_workers <= 0)
postMessage(result); // 結束
}
core.js:
var start;
onmessage = getStart;
function getStart(event) {
start = event.data;
console.log("start: " + start);
onmessage = getEnd;
}
var end;
function getEnd(event) {
end = event.data;
console.log("end: " + end);
onmessage = null;
work();
}
function work() {
console.log("finish: start=" + start + ", end=" + end);
var result = 0;
for (var i = start; i < end; i++) {
result += 1;
}
postMessage(result);
close();
}
包含其他腳本(引入腳本和庫):
可以調用importScripts(urls)方法,這個方法接收一個或多個指向JavaScript檔案的URL,如:
importScripts("file1.js", "file2.js", "file3.js");
每個加載過程都是異步進行的,所有腳本加載并執行之後,importScripts()才會傳回,但該方法本身是同步的方案;
執行的時候會按照importScirpts()方法裡參數的先後順序執行;
如果載入腳本的時候抛出了網絡錯誤,或者在執行的時候抛出了錯誤,那麼剩下的腳本就不會載入和運作;
而且,這些腳本是在Worker的全局作用域中執行;
其error事件名為onmessageerror
可以利用importScripts()方法,把一個大的worker線程,拆分成若幹個部分,每個部分負責自身的邏輯;
通過importScripts()方法載入的腳本自身還可以調用importScripts()方法載入它需要的檔案;
但是,要注意的是,importScripts()方法不會試圖去跟蹤哪些腳本已經載入了,也不會去防止循環依賴的問題;
Worder執行模型:
Worker線程從上到下同步運作它們的代碼(以及所有導入的腳本),然後進入一個異步階段,來對事件以及計時器做出響應;如果Worker注冊了onmessage事件,隻要該事件觸發了,那麼它将永遠不會退出;但是,如果Worker沒有監聽任何消息,那麼,一直到所有任務相關的回調函數都調用以及再也沒有挂起的任務(比如下載下傳和計時器)之後,它就會退出;但如果在執行完任務後,又有新任務了,它還要繼續執行,不會退出;
Worker的資料傳輸:
結構複制算法:Worker内部的運作機制是,先将消息内容串行化,然後把串行化後的字元串發給Worker,後者再将它還原,如此,傳送前和傳送後的對象,都是獨立的,互不影響,如:
主線程:
var person = {
name: "wangwei",
age: 18
}
worker.postMessage(person);
worker線程:
this.onmessage = function(event){
event.data.name = "jingjing";
console.log(event.data);
}
這種方式被稱為結構化複制算法;其特點是:
- 對象裡不能含有方法,也不能拷貝方法;
- 對象裡不能含有symbol,也不能拷貝symbol,鍵為symbol的屬性也會被忽略;
- 自定義對象的類資訊會丢失,如:傳遞一個obj=new Person(),收到的資料将沒有Person這個類資訊;但如果是一個内置對象,如Number,Boolean這樣的對象,則不會丢失!
- 不可枚舉的屬性(enumerable為false)将會被忽略;
- 屬性的可寫性配置(writable配置)将丢失;
主線程:
var person = {
name: "wangwei",
age: 18,
// 抛出異常,不能被clone
// smoking: function(){console.log("紅南京")},
// 抛出異常,不能被clone
// symbol: Symbol("zeronetwork"),
company: {
name: "零點程式員",
address: "北京東城"
}
};
person.sex = true;
Object.defineProperty(person, "salary", {
value: 8000,
writable: false,
configurable: false,
enumerable: true // 如果為false,就不可複制
});
worker.postMessage(person);
worker.onmessage = function (event) {
console.log(event.data);
};
worker線程:
self.onmessage = function(event) {
// 傳過來之後,就可以修改了,因為指定的writable配置被丢棄了
event.data.salary = 20000;
console.log(event.data);
};
主線程與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(event) {
var uInt8Array = event.data;
postMessage('worker.js: ' + uInt8Array.toString());
postMessage('uInt8Array.byteLength: ' + uInt8Array.byteLength);
for (var i = 0; i < uInt8Array.length; ++i) {
uInt8Array[i] = 0;
}
postMessage('update: ' + uInt8Array.toString());
};
傳值(拷貝)方式在發送二進制資料,會造成性能問題;
通過轉移所有權(可轉移對象)來傳遞資料(可轉移也稱為可轉讓):
為了解決這個問題,JavaScript 允許主線程把二進制資料直接轉移給子線程,但是一旦轉移,主線程就無法再使用這些二進制資料了,這是為了防止出現多個線程同時修改資料;這種轉移資料的方法,叫做Transferable Objects可轉移對象;可轉移對象從一個上下文轉移到另一個上下文而不會經過任何拷貝操作,這意味着當傳遞大資料時會獲得極大的性能提升;,對于影像處理、聲音處理、3D 運算等就非常友善了,不會産生性能負擔;
使用postMessage(message[, transfer])方法的第二個參數transfer,其是Transferable對象的數組,用于傳遞所有權,可轉移對象可以是ArrayBuffer,MessagePort或ImageBitmap等執行個體對象,如: 主線程:
var worker = new Worker('worker.js');
var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
for (var i = 0; i < uInt8Array .length; ++i) {
uInt8Array[i] = i;
}
console.log(uInt8Array.length);
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
console.log(uInt8Array.length);
worker.onmessage = function (event) {
console.log(event.data);
};
worker線程:
self.onmessage = function(event) {
var buffer = event.data;
console.log(buffer.byteLength);
postMessage(buffer, [buffer]);
console.log(buffer.byteLength);
};
可轉移對象的類型:
ArrayBuffer、MessagePort、ReadableStream、WritableStream、TransformStream、AudioData、ImageBitmap、VideoFrame、OffscreenCanvas、RTCDataChannel。
嵌入式(内聯) worker:
可以把worker代碼嵌入到主文檔中,主要是借助<script>标簽,即為該标簽的type屬性指定成一個不可運作的mime-type,如此,它就會被認為是一個資料塊元素,并且能夠被JavaScript 使用;根據這個特性,再通過blob URL對象,就可以嵌入一個 worker,如:
<script id="worker" type="javascript/worker">
onmessage = function(event){
console.log(event.data);
postMessage("worker: " + event.data);
}
</script>
<script>
var workerScript = document.querySelector('#worker').textContent;
console.log(workerScript);
var blob = new Blob([workerScript], {type: "text/javascript"});
console.log(blob); // Blob
var worker = new Worker(window.URL.createObjectURL(blob));
console.log(worker); // Worker
worker.postMessage("main發送的消息");
worker.onmessage = function(event){
console.log(event.data);
}
</script>
由于嵌入式worker不需要從遠端資源載入,是以這使得worker初始化更快;或者不使用<script>,直接使用包含代碼的字元串:(使用反引号)
var myTask = `
onmessage = function(event) {
console.log('worker:' + event.data);
postMessage("worker: " + event.data)
};
`;
var blob = new Blob([myTask]);
var worker = new Worker(window.URL.createObjectURL(blob));
console.log(worker); // Worker
worker.postMessage("main發送的消息");
worker.onmessage = function(event){
console.log(event.data);
}
簡寫形式,如:
var worker = new Worker(window.URL.createObjectURL(new Blob([`onmessage = function(event){console.log('worker:' + event.data); postMessage("worker: " + event.data)};`])));
console.log(worker); // Worker
worker.postMessage("main發送的消息");
worker.onmessage = function(event){
console.log(event.data);
}
可以同時使用多個<script>資料塊,例如:
<div id="logDisplay"></div>
<script type="text/js-worker">
var myVar = "零點程式員";
</script>
<script type="text/js-worker">
onmessage = function(event){
console.log(event.data);
postMessage(myVar);
};
</script>
<script>
// 擷取所有的type="text/js-worker"的<script>,并放一數組中,傳給Blob()
var blob = new Blob(
Array.prototype.map.call(
document.querySelectorAll("script[type=\"text\/js-worker\"]"),
function(oScript){
return oScript.textContent;
}),
{type: "text/javascript"}
);
// 建立的worker放到document.worker中
document.worker = new Worker(window.URL.createObjectURL(blob));
document.worker.onmessage = function(e){
pageLog("Received: " + e.data);
}
// 啟動worker
window.onload = function(){
document.worker.postMessage("内聯worker");
}
// 操作DOM的函數
function pageLog(msg){
var frag = document.createDocumentFragment();
frag.appendChild(document.createTextNode(msg));
frag.appendChild(document.createElement("br"));
document.querySelector("#logDisplay").appendChild(frag);
}
</script>
在執行個體化Worker時,還可以直接把worker字元串作為參數,如:
var worker = new Worker('data:text/javascript;charset=UTF-8,onmessage = function(event){postMessage("worker: 收到的資料 " + event.data);}');
worker.postMessage("零點網絡");
worker.onmessage = function(event){
console.log("Main: " + event.data);
};
Worker與Ajax:
表面上看,Ajax和Web Worker 都是異步執行的,但兩者還是有很大的差別;Ajax是異步的,但是它的運作還是單線程的;
Worker是多線程的,其子線程與主線程是分開的,是以不管子線程運作是否阻塞,還是主線程阻塞,它們之間不會互相影響;
有些情況下,Worker還需要與伺服器端進行互動,比如頁面可能需要處理來自資料庫中的資訊,此時就需要使用Ajax技術與伺服器互動;但XMLHttpRequest的responseXML和channel這兩個屬性的值為null,如:
var worker = new Worker("worker.js");
var url = "example.json";
worker.postMessage(url);
worker.onmessage = function(event){
console.log(event.data);
}
worker.js:
var xhr = new XMLHttpRequest();
xhr.onload = function(event){
self.postMessage(xhr.response);
};
onmessage = function(event){
var url = event.data;
xhr.open("GET", url);
xhr.send(null);
}
完善,首頁面:
var worker = new Worker("worker.js");
worker.onmessage = function(event){
console.log(event.data);
};
// var data = {
// method: "GET",
// url: "example.json"
// }
// worker.postMessage(data);
var data = {
method: "POST",
url: "example.php",
params: "name=wangwei&sex=男&age=18"
};
worker.postMessage(data);
worker.js:
var xhr = new XMLHttpRequest();
xhr.onload = function(event){
self.postMessage(xhr.response);
};
onmessage = function(event){
var data = event.data;
xhr.open(data.method, data.url);
var params = data.params || null;
if(params)
xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");
xhr.send(params);
}
在Worker中發起同步XMLHttpRequest;主線程:
<script>
function getContent(){
var urls = ["demo.txt","demo.json","demo.php"];
var worker = new Worker("worker.js");
worker.postMessage(urls);
worker.onmessage = function(event){
// console.log(event.data); // [...]
var contents = event.data;
for(var i=0; i<contents.length; i++){
var content;
try {
content = JSON.parse(contents[i]);
} catch (error) {
content = contents[i];
}
console.log(content);
}
}
}
</script>
<input type="button" onclick="getContent()" value="擷取内容" />
Worker線程:
onmessage = function(e){
var urls = e.data;
var contents = []; // 輸出
for(var i=0; i<urls.length; i++){
var url = urls[i];
var xhr = new XMLHttpRequest();
xhr.open("GET", url, false); // 同步XHR
xhr.send();
if(xhr.status !== 200){
throw Error(xhr.status + " " + xhr.statusText + ": " + url);
}
contents.push(xhr.responseText);
}
postMessage(contents);
}
示例:在嵌入式worker中使用Ajax:
var asyncEval = (function(){
var aListeners = [];
var worker = new Worker('data:text/javascript;charset=UTF-8,onmessage=function(e){postMessage({"id": e.data.id, "evaluated": eval(e.data.code)});}')
worker.onmessage = function(e){
if(aListeners[e.data.id])
aListeners[e.data.id](e.data.evaluated);
delete aListeners[e.data.id];
};
return function(sCode, fListener){
aListeners.push(fListener || null);
worker.postMessage({
"id": aListeners.length - 1,
"code": sCode
});
};
})();
// 應用
asyncEval("3 + 2", function(sMessage){
alert("3 + 2 = " + sMessage);
});
asyncEval('"大師哥"', function(sHTML){
document.body.appendChild(document.createTextNode(sHTML));
});
asyncEval('(function(){var xhr = new XMLHttpRequest();xhr.open("GET", "http://localhost/demo.php", false);xhr.send(null);return xhr.responseText;})()',
function(message){
console.log(message);
});
示例:在一個Worker線程中執行長時間計算;主線程:
<script>
function smear(img){
var canvas = document.createElement("canvas");
canvas.width = img.width;
canvas.height = img.height;
var context = canvas.getContext("2d");
context.drawImage(img, 0, 0);
var pixels = context.getImageData(0,0,img.width,img.height);
var worker = new Worker("smearWorker.js");
worker.postMessage(pixels);
worker.onmessage = function(e){
var smeared_pixels = e.data;
context.putImageData(smeared_pixels,0,0);
img.src = canvas.toDataURL();
worker.terminate();
canvas.width = canvas.height = 0;
}
}
</script>
<img src="images/1.jpg" onclick="smear(this)" />
線程smearWorker.js:
onmessage = function(e){
postMessage(smear(e.data));
};
function smear(pixels){
var data = pixels.data,
width = pixels.width,
height = pixels.height;
var n = 10,
m = n - 1;
for(var row = 0; row < height; row++){
var i = row*width*4 + 4;
for(var col=1; col<width; col++, i+=4){
data[i] = (data[i] + data[i-4]*m)/n;
data[i+1] = (data[i+1] + data[i-3]*m)/n;
data[i+2] = (data[i+2] + data[i-2]*m)/n;
data[i+3] = (data[i+3] + data[i-1]*m)/n;
}
}
return pixels;
}
共享Worker(Shared Worker):
一個Shared Worker可以被多個腳本使用,也就是可以被不同的視窗的多個腳本運作,例如window、Iframes或worker等,可以實作多頁面之間的資料共享與通信;共享worker 比專用 worker 稍微複雜一點,它的腳本必須通過活動端口進行通訊;共享worker的上下文是SharedWorkerGlobalScope對象,也是繼承自WorkerGlobalScope類;
建立SharedWorker:
使用SharedWorker(URL[, name | options]) 構造函數執行個體化一個SharedWorker 對象,參數URL指定了要執行URL腳本,所執行的腳本必須遵守同源政策;參數name表示worker範圍的辨別名稱,主要用于調試;參數options為可選,與Worker()參數中的options相同;如:
var sharedWorker = new SharedWorker("worker.js");
console.log(sharedWorker); // SharedWorker
port屬性:隻讀,傳回一個MessagePort對象,該對象可以用來進行通信,并能對共享worker進行控制;
console.log(sharedWorker.port); // MessagePort
MessagePort類型:MessagePort 是屬于Channel Messaging API當中的一個接口;
MessagePort事件:
onmessage:當該端口收到了一條消息時觸發;
onmessageerror:當端口收到了一條無法被反序列化的消息時觸發;
sharedWorker.port.onmessage = function(event){
console.log(event); // MessageEvent
console.log(event.data);
};
sharedWorker.port.onmessageerror = function(event){
console.log(event);
};
onmessage事件中的事件對象類型是MessageEvent類型;
MessagePort方法:
啟動端口時,可以使用postMessage()方法:從目前端口發送一條消息:
sharedWorker.port.postMessage("Main: 發送的資料");
sharedWorker.port.onmessage = function(event){
console.log("Main: " + event.data);
};
start():在傳遞消息前,端口連接配接必須被顯式打開,即使用start()方法開始發送該端口中的消息;
sharedWorker.port.start();
當使用onmessage注冊事件,預設port就已經打開了;如果使用addEventListener注冊message事件,就必須使用start()方法打開port;如:
myWorker.port.start();
myWorker.port.addEventListener("message",function(event){
console.log(event.data);
});
是以,推薦使用DOM1級事件注冊機制;
在使用start()方法打開端口連接配接時,如果首頁面和SharedWorker需要雙向通信,那麼它們都需要調用start()方法,如:
// ...
port.start(); // worker線程中的調用, 假設port變量代表一個端口
每個新的SharedWorker連接配接都觸發一個onconnect事件;
在Worker中使用onconnect處理程式連接配接到相關端口;可以用SharedWorker内部的onconnect 事件的event對象的ports屬性中擷取到與該SharedWorker相連接配接的端口;
使用ports[0]擷取目前使用的port,即MessagePort對象,如:
onconnect = function(event){
var port = event.ports[0];
port.postMessage("ports" + event.ports); // ports[object MessagePort]
port.postMessage(event.ports.length); // 1
};
發送資料使用port對象的postMessage()方法,如:
onconnect = function(event){
var port = event.ports[0];
port.postMessage("worker start");
};
使用port對象start()方法來啟動端口或直接使用onmessage 處理程式處理來自主線程的消息,并可以向首頁面傳遞消息,如:
onconnect = function(e) {
var port = e.ports[0];
port.addEventListener("message", function(e) {
port.postMessage("接收到資料為:" + e.data);
});
port.start();
}
但還是建議DOM1級事件注冊,如:
onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(event){
port.postMessage("warker: 接收到的消息再傳回:" + event.data);
}
};
SharedWorkerGlobalScope:
在共享worker内部,全局作用域是SharedWorkerGlobalScope的一個執行個體,它繼承自WorkerGlobalScope ,是以包括它的所有屬性和方法;和專用worker一樣,共享worker可以通過self通路全局作用域;
共享資料:index.html和index1.html,兩個頁面結構一樣,如:
<p><input type="button" value="點選一下" id="btn" /></p>
<p><span>點了 <span class="time">-</span> 下</span></p>
<script>
var btn = document.getElementById("btn");
var worker = new SharedWorker('worker.js');
btn.addEventListener('click', function () {
worker.port.postMessage('start');
});
var timeDom = document.querySelector('.time');
worker.port.onmessage = function (event) {
timeDom.innerHTML = event.data;
}
</script>
worker線程:
var a = 1; // 全局變量
onconnect = function (e) {
var port = e.ports[0];
port.onmessage = function () {
port.postMessage(a++)
}
}
示例:點贊,首頁面:
<button id="likeBtn">點贊</button>
<p>一共收獲了<span id="likedCount">0</span>個贊</p>
<script>
var likeBtn = document.querySelector("#likeBtn");
var likedCount = document.querySelector("#likedCount");
var sharedworker = new SharedWorker("sharedworker.js");
console.log("worker.port: ", sharedworker.port);
likeBtn.addEventListener("click", function(){
sharedworker.port.postMessage("like");
});
sharedworker.port.onmessage = function(event){
likedCount.innerHTML = event.data;
};
</script>
sharedworker.js:
var count = 666;
self.onconnect = function(event){
var port = event.ports[0];
port.postMessage(count);
port.onmessage = function(e){
port.postMessage(++count);
}
}
在SharedWorker中,可以根據擷取到不同的值,發送不同的資料,如:首頁面:
var sharedWorker = new SharedWorker("sharedworker.js");
sharedWorker.port.onmessage = function(event){
console.log("來自worker的消息:" + event.data);
};
sharedWorker.port.postMessage("get"); // A頁面用
sharedWorker.port.postMessage("發送新資料給worker"); // B頁面用
worker.js:
var data = 'worker中原始資料';
self.onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
if(e.data === "get"){
port.postMessage(data);
}else{
data = e.data;
port.postMessage("worker: " + data);
// 後續處理
}
}
}
Dedicated Worker與Shared Worker的本質差別:
專用Worker隻屬于某個頁面,不會和其他頁面的Render程序(浏覽器核心程序)共享,是以在Render程序中建立一個新的線程來運作Worker中的JavaScript程式;
SharedWorker是浏覽器所有頁面共享的,不能采用與Worker同樣的方式實作,因為它不隸屬于某個Render程序,可以為多個Render程序共享使用,是以浏覽器為SharedWorker單獨建立一個程序來運作JavaScript程式,在浏覽器中使用相同名字的SharedWorker的多個腳本,隻存在一個SharedWorker程序,不管它被建立多少次;
共享worker身份由解析腳本URL、worker名和文檔源确定;
new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js');
new SharedWorker('sharedWorker.js');
new SharedWorker('http://localhost/sharedWorker.js');
即使加載的是同一個腳本,但解析出來的URL不一緻,也不能共享,如:
new SharedWorker('./sharedWorker.js');
new SharedWorker('./sharedWorker.js?');
因為可選的worker名是共享worker身份的一部分,使用不同的worker名會讓浏覽器強制建立多個共享worker,盡管它們的源和腳本URL相同;
new SharedWorker('./sharedWorker.js', {name: 'one'});
new SharedWorker('./sharedWorker.js', {name: 'one'});
new SharedWorker('./sharedWorker.js', {name: 'two'});
共享worker可以在标簽、視窗、iframe或其他運作在同一個源的worker之間共享; 如:index.html
<iframe src="index1.html" width="600" height="300" frameborder="0"></iframe>
SharedWorker也可以使用importScripts()方法;但不能使用共享subworker,在某些浏覽器中,可以使用專用subworker,例如Firefox可以使用,如:首頁面:
var sharedWorker = new SharedWorker('worker.js');
sharedWorker.port.onmessage = function(event){
console.log("Main: " + event.data);
};
worker.js:
importScripts("w1.js","w2.js");
onconnect = function (e) {
var port = e.ports[0];
port.postMessage("返給首頁面,我用到了subworker");
var subworker = new Worker("subworker.js");
subworker.onmessage = function(event){
console.log("worker: " + event.data);
port.postMessage("worker轉發給main: " + event.data);
};
subworker.postMessage("worker發送給subworker");
}
subworker.js:
onmessage = function(event){
console.log("subworker: " + event.data);
postMessage("subworker發送給worker");
};
在一個腳本中,可以同時使用專用Worker和共享Worker;如:首頁面index.html:
var worker = new Worker('worker.js');
var sharedWorker = new SharedWorker("sharedworker.js");
worker.onmessage = function(event){
console.log(event.data);
// 再扔給sharedWorker
sharedWorker.port.postMessage(event.data);
sharedWorker.port.onmessage = function(e){
console.log(e.data);
};
};
// 給專用worker一個對象
worker.postMessage({name:"wangwei",age:18});
worker線程:
onmessage = function(event){
var person = event.data;
// 進行必要的處理,如:
person.sex = true;
person.company = {
name: "零點程式員",
address: "北京東城"
}
// 加工一下,再送回去
postMessage(person);
};
sharedworker.js:
var obj;
onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
if(!obj)
obj = e.data;
port.postMessage(obj);
}
}
close()方法:斷開端口連接配接,它将不再是激活狀态;如:
// 首頁面:
worker.port.close() // 僅僅關閉連接配接
// SharedWorker内部:
port.close() // 僅僅關閉連接配接
不能人為終止共享SharedWorker,在MessagPort對象上調用close()也不會導緻共享worker的終止,它僅僅是斷開連接配接,并不會銷毀SharedWorker,即使隻有一個頁面和共享worker連接配接,其還會持續運作;
另外SharedWorker沒有類似terminate()的方法;其運作的狀态與相應的MessagePort或MessageChannel的狀态無關;一旦建立并連接配接SharedWorker,連接配接的管理就由浏覽器負責,并且所建立的連接配接将持續在頁面的生命周期中持續,隻有全部頁面銷毀,沒有連接配接的時候,且該SharedWorker内部沒有運作中的任務,浏覽器才終止該SharedWorker;
Worker(專用和共享)内部中的事件,主線程是沒法監聽到的,反之亦然;Worker中的異常,主線程也是無法感覺的,反之亦然;二者唯一的互動方式就是postMessage和監聽message事件;如:首頁面:
<input type="button" value="發送消息" id="btnSend">
<input type="button" value="關閉連接配接" id="btnClose">
<script>
var sharedWorker = new SharedWorker("sharedworker.js");
sharedWorker.port.onmessage = function(event){
console.log(event.data);
};
var btnSend = document.getElementById("btnSend");
btnSend.onclick = function(){
sharedWorker.port.postMessage("main發送的消息");
}
var btnClose = document.getElementById("btnClose");
btnClose.onclick = function(){
sharedWorker.port.close();;
console.log("sharedWorker已關閉");
}
</script>
sharedworker:
var connectList = [];
onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
port.postMessage("收到消息: " + e.data);
if(connectList.indexOf(port) === -1)
connectList.push(port);
connectList.forEach(function(v,i){
v.postMessage("現在有連接配接: " + connectList.length);
})
}
}
清除Shared Worker:
在頁面關閉後,workerPool中的port并不會自動清除,會造成記憶體的占用;通過監聽頁面關閉或頁面生命周期的window.onbeforeunload,通知SharedWorker關閉此連接配接;如:首頁面:
var btnClose = document.getElementById("btnClose");
btnClose.onclick = function(){
sharedWorker.port.postMessage({action: "close"});
sharedWorker.port.close();
console.log("sharedWorker已關閉");
}
sharedworker.js
var connectList = [];
onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
if(connectList.indexOf(port) === -1)
connectList.push(port);
if(e.data.action == "close"){
port.close();
connectList = connectList.filter(function(item){
return item != port;
})
}
connectList.forEach(function(v,i){
v.postMessage("現在有連接配接: " + connectList.length);
});
}
}
當關閉視窗時,包括重新整理重新載入,也需要通知SharedWorker,如:首頁面添加:
window.onbeforeunload = function(){
sharedWorker.port.postMessage({
action: "close"
})
}
錯誤的處理:
SharedWorker内部的錯誤,無論是其内部還是首頁面都無法擷取,如:首頁面:
var sharedWorker = new SharedWorker("sharedworker.js");
console.log(sharedWorker);
console.log(sharedWorker.port);
sharedWorker.port.postMessage("随便發點啥");
sharedWorker.port.onmessage = function(event){
console.log(event.data);
};
sharedWorker.onerror = function(event){
console.log(event);
};
sharedWorker.port.onerror = function(event){
console.log(event);
};
sharedWorker.port.onmessageerror = function(event){
console.log(event);
};
sharedworker.js:
onconnect = function(event){
var port = event.ports[0];
console.log(port);
port.onmessage = function(e){
throw new Error("有錯了");
};
port.onmessageerror = function(event){
console.log(event);
}
}
sharedWorker.onerror,在共享工作程序中發生錯誤時觸發;
port.onmessageerror事件,當MessagePort對象接收到無法反序列化的消息時觸發;
示例:計算兩個數的積
var first = document.querySelector('#number1');
var second = document.querySelector('#number2');
var result = document.querySelector('.result');
if(!!window.SharedWorker) {
var myWorker = new SharedWorker("sharedworker.js");
first.onchange = function() {
myWorker.port.postMessage([first.value, second.value]);
console.log('向worker中發送第一個乘數');
}
second.onchange = function() {
myWorker.port.postMessage([first.value, second.value]);
console.log('向worker中發送第二個乘數');
}
myWorker.port.onmessage = function(event) {
result.textContent = event.data;
console.log('從worker接收到計算結果:' + event.data);
}
}else{
result.textContent = '浏覽器不支援 web workers';
}
sharedworker2.html:
<div class="controls">
<div>
<label for="number3">平方數: </label>
<input type="text" id="number3" value="0">
</div>
<p>上一次結果: <span class="result">-</span></p>
</div>
<script>
var squareNumber = document.querySelector('#number3');
var result = document.querySelector('.result');
if (!!window.SharedWorker) {
var myWorker = new SharedWorker("sharedworker.js");
squareNumber.onchange = function() {
myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
console.log('發送資料到Worker中');
}
myWorker.port.onmessage = function(e) {
result.textContent = e.data;
console.log('從worker接收資料');
}
}
</script>
sharedworker.js:
var result = 0;
onconnect = function(event) {
var port = event.ports[0];
port.postMessage(result);
port.onmessage = function(e) {
result = e.data[0] * e.data[1];
port.postMessage(result);
}
}
在SharedWorker中,利用ports屬性向所有連接配接頁面分發消息,如:
<div id="message"></div>
<p><input type="text" class="input" />
<button id="btn">送出</button></p>
<p><a href="worker2.html" target="_blank">worker2.html</a></p>
<script>
function showNews(data){
document.querySelector("#message").innerText = JSON.stringify(data);
}
var worker = new SharedWorker("sharedworker.js", "work");
worker.port.postMessage({
status: 0,
});
worker.port.onmessage = function(event){
showNews(event.data);
};
document.querySelector("#btn").addEventListener("click", function(event){
worker.port.postMessage({
status: 2,
value: document.querySelector(".input").value
});
});
</script>
sharedworker.js:
var connectList = [];
var textList = [];
onconnect = function(event){
var port = event.ports[0];
port.onmessage = function(e){
if(connectList.indexOf(port) === -1)
connectList.push(port);
switch(e.data.status){
case 0:
postMsg(function(item){
if(item != port)
item.postMessage('有新使用者加入');
else
item.postMessage("我是新使用者");
});
break;
default:
textList.push(e.data.value);
postMsg(textList);
break;
}
}
}
// 分發消息
function postMsg(callback){
var callback = (typeof callback === "function") ? callback : function(item){
item.postMessage(callback);
}
connectList.forEach(callback);
}
示例:頁面之間登入狀态通知:首頁面:
<style>
*{margin:0; padding:0}
.container{
background: rgb(38, 7, 95) url("images/bg.png") no-repeat center top;
background-size: contain; height: 1000px;
}
.tips{
display: none;
height:80px; width:100%; text-align:center; line-height: 80px;
background-color:bisque; position:fixed; top:0;
}
</style>
<div class="container"></div>
<div class="tips">login info...</div>
<script>
var sharedWorker = new SharedWorker("sharedworker.js");
sharedWorker.port.onmessage = function(event){
console.log(event.data);
var tips = document.querySelector(".tips");
tips.innerText = event.data.tipsText;
tips.style.display = "block";
};
</script>
login.html登入頁:
<input type="button" value="登入" id="btnLogin">
<input type="button" value="退出" id="btnLogout">
<script>
var sharedWorker = new SharedWorker("sharedworker.js");
sharedWorker.port.onmessage = function(event){
console.log(event.data);
if(event.data.isLogin){
alert('登入成功,想幹啥就幹啥');
}else{
alert('使用者退出,回到首頁');
location.href = "/";
}
}
var btnLogin = document.getElementById("btnLogin");
btnLogin.onclick = function(event){
sharedWorker.port.postMessage({isLogin: true});
}
var btnLogin = document.getElementById("btnLogin");
btnLogout.onclick = function(){
sharedWorker.port.postMessage({isLogin: false});
}
</script>
sharedworker.js:
var isLogin = false;
var connectList = [];
var tipsTextArray = [
"You signed in another tab or window. Reload to refresh your session.",
"You signed out in another tab or window. Reload to refresh your session."
];
onconnect = function(event){
var port = event.ports[0];
if(connectList.indexOf(port) === -1)
connectList.push(port);
port.onmessage = function(e){
isLogin = e.data.isLogin;
tipsText = isLogin ? tipsTextArray[0] : tipsTextArray[1];
connectList.forEach(function(item){
item.postMessage({isLogin: isLogin, tipsText: tipsText});
})
};
}