專用Web Worker (Dedicated Web Worker) 提供了一個簡單的方法使得web 內容能夠在後台運行腳本。一旦worker 創建後,它可以向由它的創建者指定的事件監聽函數傳遞消息,這樣該worker 生成的所有任務就都會接收到這些消息。 worker 線程能夠在不干擾UI 的情況下執行任務。另外,它還能夠使用XMLHttpRequest(雖然responseXML與channel 兩個屬性值始終是null)來執行I/O 操作。本文通過提供例子和細節補全了前面的文檔。提供給worker 的函數列出了worker 所支持的函數。
Worker接口會生成真正的操作系統級別的線程,如果你不太小心,那麼並發(concurrency)會對你的代碼產生有趣的影響。然而,對於web worker 來說,與其他線程的通信點會被很小心的控制,這意味著你很難引起並發問題。你沒有辦法去訪問非線程安全的組件或者是DOM,此外你還需要通過序列化對象來與線程交互特定的數據。所以你要是不費點勁兒,還真搞不出錯誤來。生成worker
創建一個新的worker 十分簡單。你所要做的就是調用Worker()構造函數,指定一個要在worker 線程內運行的腳本的URI,如果你希望能夠收到worker 的通知,可以將worker 的onmessage屬性設置成一個特定的事件處理函數。
var myWorker = new Worker(my_task.js);myWorker.onmessage = function (oEvent) { console.log(Called back by the worker!/n);};或者,你也可以使用addEventListener():
var myWorker = new Worker(my_task.js);myWorker.addEventListener(message, function (oEvent) { console.log(Called back by the worker!/n);}, false);myWorker.postMessage(); // start the worker.例子中的第一行創建了一個新的worker 線程。第三行為worker 設置了message事件的監聽函數。當worker 調用自己的postMessage() 函數時就會調用這個事件處理函數。最後,第七行啟動了worker 線程。注意: 傳入Worker構造函數的參數URI 必須遵循同源策略。目前,不同的瀏覽器製造商對於哪些URI 應該遵循同源策略尚有分歧;Gecko 10.0 (Firefox 10.0 / Thunderbird 10.0 / SeaMonkey 2.7) 及後續版本允許傳入data URI,而Internet Explorer 10 則不認為Blob URI對於worker 來說是一個有效的腳本。
傳遞數據在主頁面與worker 之間傳遞的數據是通過拷貝,而不是共享來完成的。傳遞給worker 的對象需要經過序列化,接下來在另一端還需要反序列化。頁面與worker不會共享同一個實例,最終的結果就是在每次通信結束時生成了數據的一個副本。大部分瀏覽器使用結構化拷貝來實現該特性。
在往下進行之前,出於教學的目的,讓我們創建一個名為emulateMessage()的函數,它將模擬在從worker到主頁面(反之亦然)的通信過程中,變量的「拷貝而非共享」行為:
function emulateMessage (vVal) { return eval(( + JSON.stringify(vVal) + ));}// Tests// test #1var example1 = new Number(3);alert(typeof example1); // objectalert(typeof emulateMessage (example1)); // number// test #2var example2 = true;alert(typeof example2); // booleanalert(typeof emulateMessage(example2)); // boolean// test #3var example3 = new String(Hello World) ;alert(typeof example3); // objectalert(typeof emulateMessage(example3)); // string// test #4var example4 = { name: John Smith, age: 43};alert(typeof example4); // objectalert(typeof emulateMessage(example4)); // object// test #5function Animal (sType, nAge) { this.type = sType; this.age = nAge;}var example5 = new Animal(Cat, 3);alert(example5.constructor ); // Animalalert(emulateMessage(example5).constructor); // Object拷貝而並非共享的那個值稱為消息。再來談談worker,你可以使用postMessage() 將消息傳遞給主線程或從主線程傳送回來。 message事件的data屬性就包含了從worker 傳回來的數據。
example.html: (主頁面): var myWorker = new Worker(my_task.js);myWorker.onmessage = function (oEvent) { console.log(Worker said : + oEvent.data);};myWorker.postMessage(ali);my_task.js (worker):postMessage (I/'m working before postMessage(/'ali/').);onmessage = function (oEvent) { postMessage(Hi + oEvent.data);};注意:通常來說,後台線程– 包括worker –無法操作DOM。如果後台線程需要修改DOM,那麼它應該將消息發送給它的創建者,讓創建者來完成這些操作。
如你所見,worker與主頁面之間傳輸的消息始終是「JSON 消息」,即使它是一個原始類型的值。所以,你完全可以傳輸JSON數據和/或任何能夠序列化的數據類型:
postMessage({cmd: init, timestamp: Date.now()});傳遞數據的例子
例子#1: 創建一個通用的「異步eval()」
下面這個例子介紹了,如何在worker 內使用eval()來按順序執行異步的任何種類的JavaScript 代碼:
// Syntax: asyncEval(code[, listener])var asyncEval = (function () { var aListeners = [], oParser = new Worker(data:text/javascript;charset=US-ASCII,onmessage%20%3D%20function %20%28oEvent%29%20%7B%0A%09postMessage%28%7B%0A%09%09%22id%22%3A%20oEvent.data.id%2C%0A%09%09%22evaluated%22%3A %20eval%28oEvent.data.code%29%0A%09%7D%29%3B%0A%7D); oParser.onmessage = function (oEvent) { if (aListeners[oEvent.data.id]) { aListeners[oEvent .data.id](oEvent.data.evaluated); } delete aListeners[oEvent.data.id]; }; return function (sCode, fListener) { aListeners.push(fListener || null); oParser.postMessage({ id : aListeners.length - 1, code: sCode }); };})();示例使用:
// asynchronous alert message...asyncEval(3 + 2, function (sMessage) { alert(3 + 2 = + sMessage);});// asynchronous print message...asyncEval(/Hello World!!!/, function (sHTML) { document.body.appendChild(document.createTextNode(sHTML));});// asynchronous void...asyncEval((function () {/n/tvar oReq = new XMLHttpRequest();/n/ toReq.open(/get/, /http://www.mozilla.org//, false);/n/toReq.send(null);/n/treturn oReq.responseText;/n})());例子#2:傳輸JSON 的高級方式和創建一個交換系統
如果你需要傳輸非常複雜的數據,還要同時在主頁與Worker 內調用多個方法,那麼可以考慮創建一個類似下面的系統。
example.html(the main page): <!doctype html><html><head><meta charset=UTF-8 /><title>MDN Example - Queryable worker</title><script type=text/javascript> /* QueryableWorker instances methods: * sendQuery(queryable function name, argument to pass 1, argument to pass 2, etc. etc): calls a Worker's queryable function * postMessage(string or JSON Data): see Worker.prototype.postMessage() * terminate(): terminates the Worker * addListener (name, function): adds a listener * removeListener(name): removes a listener QueryableWorker instances properties: * defaultListener: the default listener executed only when the Worker calls the postMessage() function directly */ function QueryableWorker (sURL, fDefListener, fOnError ) { var oInstance = this, oWorker = new Worker(sURL), oListeners = {}; this.defaultListener = fDefListener || function () {}; oWorker.onmessage = function (oEvent) { if (oEvent.data instanceof Object && oEvent.data.hasOwnProperty(vo42t30) && oEvent.data.hasOwnProperty(rnb93qh)) { oListeners[oEvent.data.vo42t30].apply(oInstance, oEvent.data.rnb93qh); } else { this.defaultListener.call(oInstance, oEvent.data); } }; if (fOnError) { oWorker.onerror = fOnError; } this.sendQuery = function (/* queryable function name, argument to pass 1, argument to pass 2, etc. etc */) { if (arguments.length < 1) { throw new TypeError(QueryableWorker.sendQuery - not enough arguments); return; } oWorker.postMessage({ bk4e1h0: arguments[0], ktp3fm1: Array.prototype.slice.call(arguments, 1) }); }; this.postMessage = function (vMsg) { //I just think there is no need to use call() method //how about just oWorker.postMessage(vMsg); //the same situation with terminate // well,just a little faster,no search up the prototye chain Worker.prototype.postMessage.call(oWorker, vMsg); }; this.terminate = function () { Worker.prototype.terminate.call(oWorker); }; this .addListener = function (sName, fListener) { oListeners[sName] = fListener; }; this.removeListener = function (sName) { delete oListeners[sName]; }; }; // your custom queryable worker var oMyTask = new QueryableWorker( my_task.js /* , yourDefaultMessageListenerHere [optional], yourErrorListenerHere [optional] */); // your custom listeners oMyTask.addListener(printSomething, function (nResult) { document.getElementById(firstLink).parentNode.appendChild(document.createTextNode( The difference is + nResult + !)); }); oMyTask.addListener(alertSomething, function (nDeltaT, sUnit) { alert(Worker waited for + nDeltaT + + sUnit + :-)); });</script>< /head><body> <ul> <li><a id=firstLink href=javascript:oMyTask.sendQuery('getDifference', 5, 3);>What is the difference between 5 and 3?</a></ li> <li><a href=javascript:oMyTask.sendQuery('waitSomething');>Wait 3 seconds</a></li> <li><a href=javascript:oMyTask.terminate();>terminate( ) the Worker</a></li> </ul></body></html>my_task.js (the worker):// your custom PRIVATE functionsfunction myPrivateFunc1 () { // do something}function myPrivateFunc2 () { // do something}// etc. etc.// your custom PUBLIC functions (ie queryable from the main page)var queryableFunctions = { // example #1: get the difference between two numbers: getDifference: function (nMinuend, nSubtrahend ) { reply(printSomething, nMinuend - nSubtrahend); }, // example #2: wait three seconds waitSomething: function () { setTimeout(function() { reply(alertSomething, 3, seconds); }, 3000); }} ;// system functionsfunction defaultQuery (vMsg) { // your default PUBLIC function executed only when main page calls the queryableWorker.postMessage() method directly // do something}function reply (/* listener name, argument to pass 1, argument to pass 2, etc. etc */) { if (arguments.length < 1) { throw new TypeError(reply - not enough arguments); return; } postMessage({ vo42t30: arguments[0], rnb93qh: Array.prototype.slice .call(arguments, 1) });}onmessage = function (oEvent) { if (oEvent.data instanceof Object && oEvent.data.hasOwnProperty(bk4e1h0) && oEvent.data.hasOwnProperty(ktp3fm1)) { queryableFunctions[oEvent.data .bk4e1h0].apply(self, oEvent.data.ktp3fm1); } else { defaultQuery(oEvent.data); }};這是一個非常合適的方法,用於切換主頁-worker - 或是相反的- 之間的消息。
通過轉讓所有權(可轉讓對象)來傳遞數據
Google Chrome 17 與Firefox 18 包含另一種性能更高的方法來將特定類型的對象(可轉讓對象) 傳遞給一個worker/從worker 傳回。可轉讓對像從一個上下文轉移到另一個上下文而不會經過任何拷貝操作。這意味著當傳遞大數據時會獲得極大的性能提升。如果你從C/C++ 世界來,那麼把它想像成按照引用傳遞。然而與按照引用傳遞不同的是,一旦對象轉讓,那麼它在原來上下文的那個版本將不復存在。該對象的所有權被轉讓到新的上下文內。例如,當你將一個ArrayBuffer對像從主應用轉讓到Worker 中,原始的ArrayBuffer被清除並且無法使用。它包含的內容會(完整無差的)傳遞給Worker 上下文。
// Create a 32MB file and fill it.var uInt8Array = new Uint8Array(1024*1024*32); // 32MBfor (var i = 0; i < uInt8Array .length; ++i) { uInt8Array[i] = i; }worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);生成subworker如果需要的話Worker 能夠生成更多的Worker。這樣的被稱為subworker,它們必須託管在與父頁面相同的源內。同理,subworker 解析URI 時會相對於父worker 的地址而不是自身的頁面。這使得worker 容易監控它們的依賴關係。 Chrome 目前並不支持subworker。
嵌入式worker目前沒有一種「官方」的方法能夠像<script>元素一樣將worker 的代碼嵌入的網頁中。但是如果一個<script>元素沒有src 特性,並且它的type特性沒有指定成一個可運行的mime-type,那麼它就會被認為是一個數據塊元素,並且能夠被JavaScript 使用。 「數據塊」是HTML5 中一個十分常見的特性,它可以攜帶幾乎任何文本類型的數據。所以,你能夠以如下方式嵌入一個worker:
<!DOCTYPE html><html><head><meta charset=UTF-8 /><title>MDN Example - Embedded worker</title><script type=text/js-worker> // 該腳本不會被JS引擎解析,因為它的mime-type 是text/js-worker。 var myVar = Hello World!; // 剩下的worker 代碼寫到這裡。 </script><script type=text/javascript> // 該腳本會被JS 引擎解析,因為它的mime-type 是text/javascript。 function pageLog (sMsg) { // 使用fragment:這樣瀏覽器只會進行一次渲染/重排。 var oFragm = document.createDocumentFragment(); oFragm.appendChild(document.createTextNode(sMsg)); oFragm.appendChild(document.createElement(br)); document.querySelector(#logDisplay).appendChild(oFragm); }</script ><script type=text/js-worker> // 該腳本不會被JS 引擎解析,因為它的mime-type 是text/js-worker。 onmessage = function (oEvent) { postMessage(myVar); }; // 剩下的worker 代碼寫到這裡。 </script><script type=text/javascript> // 該腳本會被JS 引擎解析,因為它的mime-type 是text/javascript。 // 在過去...: // 我們使用blob builder // ...但是現在我們使用Blob...: var blob = new Blob(Array.prototype.map.call(document.querySelectorAll(script[type= /text//js-worker/]), function (oScript) { return oScript.textContent; }),{type: text/javascript}); // 創建一個新的document.worker 屬性,包含所有text/js- worker 腳本。 document.worker = new Worker(window.URL.createObjectURL(blob)); document.worker.onmessage = function (oEvent) { pageLog(Received: + oEvent.data); }; // 啟動worker. window.onload = function () { document.worker.postMessage(); };</script></head><body><div id=logDisplay></div></body></html>現在,嵌入式worker 已經嵌套進了一個自定義的document.worker屬性中。
超時與間隔Worker 能夠像主線程一樣使用超時與間隔。這會十分有用,比如說,如果你想讓worker 線程週期性而並非不間斷的運行代碼。
終止worker如果你想立即終止一個運行中的worker,可以調用worker 的terminate()方法:
myWorker.terminate();
worker 線程會被立即殺死,不會留下任何機會讓它完成自己的操作或清理工作。
Workers 也可以調用自己的nsIWorkerScope.close()方法來關閉自己:
self.close();處理錯誤
當worker 出現運行時錯誤時,它的onerror事件處理函數會被調用。它會收到一個實現了ErrorEvent接口名為error的事件。該事件不會冒泡,並且可以被取消;為了防止觸發默認動作,worker 可以調用錯誤事件的preventDefault()方法。錯誤事件擁有下列三個它感興趣的字段:
message可讀性良好的錯誤消息。
filename發生錯誤的腳本文件名。
lineno發生錯誤時所在腳本文件的行號。
訪問navigator 對象Workers 可以在它的作用域內訪問navigator對象。它含有如下能夠識別瀏覽器的字符串,就像在普通腳本中做的那樣:
Worker 線程能夠訪問一個全局函數,importScripts(),該函數允許worker 將腳本或庫引入自己的作用域內。你可以不傳入參數,或傳入多個腳本的URI 來引入;以下的例子都是合法的:
importScripts(); /* 什麼都不引入*/importScripts('foo.js'); /* 只引入foo.js */importScripts('foo.js', 'bar.js'); /* 引入兩個腳本*/瀏覽器將列出的腳本加載並運行。每個腳本中的全局對像都能夠被worker 使用。如果腳本無法加載,將拋出NETWORK_ERROR異常,接下來的代碼也無法執行。而之前執行的代碼(包括使用window.setTimeout()延遲執行的代碼)卻依然能夠使用。 importScripts()之後的函數聲明依然能夠使用,因為它們始終會在其他代碼之前運行。注意:腳本的下載順序不固定,但執行時會按照你將文件名傳入到importScripts()中的順序。這是同步完成的;直到所有腳本都下載並運行完畢,importScripts()才會返回。
例子本節提供了幾個如何使用DOM worker 的例子。
在後台執行運算
worker 的一個優勢在於能夠執行處理器密集型的運算而不會阻塞UI 線程。在下面的例子中,worker 用於計算斐波那契數。
JavaScript 代碼
下面的JavaScript 代碼保存在「fibonacci.js」文件中,與下一節的HTML 文件關聯。
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); } };worker 將屬性onmessage設置為一個函數,當worker 對象調用postMessage() 時該函數會接收到發送過來的信息。 (注意,這麼使用並不等同於定義一個同名的全局變量,或是定義一個同名的函數。var onmessage與function onmessage將會定義與該名字相同的全局屬性,但是它們不會註冊能夠接收從創建worker的網頁發送過來的消息的函數。) 這會啟用遞歸,生成自己的新拷貝來處理計算的每一個循環。
HTML 代碼
<!DOCTYPE html><html> <head> <meta charset=UTF-8 /> <title>Test threads fibonacci</title> </head> <body> <div id=result></div> <script language =javascript> var worker = new Worker(fibonacci.js); worker.onmessage = function(event) { document.getElementById(result).textContent = event.data; dump(Got: + event.data + /n); } ; worker.onerror = function(error) { dump(Worker error: + error.message + /n); throw error; }; worker.postMessage(5); </script> </body></html>網頁創建了一個div元素,ID 為result, 用它來顯示運算結果,然後生成worker。在生成worker 後,onmessage處理函數配置為通過設置div元素的內容來顯示運算結果,然後onerror處理函數被設置為轉儲錯誤信息。最後,向worker 發送一條信息來啟動它。