Web Worker
前面的話
客戶端javascript其中一個基本的特性就是單線程:比如,瀏覽器無法同時運行兩個事件處理程序,它也無法在一個事件處理程序運行的時候觸發一個計時器。Web Worker是HTML5提供的一個javascript多線程解決方案,可以將一些大計算量的代碼交由web Worker運行從而避免阻塞用戶界面,在執行復雜計算和數據處理時,這個API非常有用。本文將詳細介紹Web Worker
前提
在使用Worker之前,首先要檢測瀏覽器是否支持這個API
[註意]IE9-瀏覽器不支持
if (window.Worker) { // to do }
使用Web Worker有以下幾點限制:
同域限制。子線程加載的腳本文件,必須與主線程的腳本文件在同一個域
DOM限制。子線程所在的全局對象,與主進程不一樣,它無法讀取網頁的DOM對象,即document、window、parent這些對象,子線程都無法得到。(但是,navigator對象和location對象可以獲得。)
腳本限制。子線程無法讀取網頁的全局變量和函數,也不能執行alert和confirm方法,不過可以執行setInterval和setTimeout,以及使用XMLHttpRequest對象發出AJAX請求
文件限制。子線程無法讀取本地文件,即子線程無法打開本機的文件系統(file://),它所加載的腳本,必須來自網絡
主線程
【新建】
主線程采用new命令,調用Worker構造函數,可以新建一個子線程
var worker = new Worker(‘work.js‘);
Worker構造函數的參數是一個腳本文件,這個文件就是子線程所要完成的任務,上面代碼中是work.js
這行代碼會導致瀏覽器下載work.js,但只有Worker接收到消息才會實際執行文件中的代碼
由於子線程不能讀取本地文件系統,所以這個腳本文件必須來自網絡端。如果下載沒有成功,比如出現404錯誤,這個子線程就會默默地失敗
【傳遞消息】
要給Worker傳遞消息,可以使用postMessage()方法。postMessage()方法的參數,就是主線程傳給子線程的信號。它可以是一個字符串,也可以是一個對象
一般來說,可以序列化為JSON結構的任何值都可以作為參數傳遞給postMessage()。換句話說,這就意味著傳入的值是被復制到Worker中,而非直接傳過去的
worker.postMessage("Hello World"); worker.postMessage({method: ‘echo‘, args: [‘Work‘]});
只要符合父線程的同源政策,Worker線程自己也能新建Worker線程。Worker線程可以使用XMLHttpRequest進行網絡I/O,但是XMLHttpRequest對象的responseXML和channel屬性總是返回null
【事件監聽】
主線程使用onmessage事件來監聽子線程發來的信息。來自子線程Worker的數據保存在event.data中。Worker返回的數據也可以是任何能夠被序列化的值
worker.addEventListener(‘message‘, function(e) { //對數據進行處理 console.log(e.data); }, false);
【錯誤處理】
如果子線程發生錯誤,會觸發主線程的error事件。error事件的事件對象中包含三個屬性:filename、lineno和message,分別表示發生錯誤的文件名、代碼行號和完整的錯誤消息
worker.onerror = function(event){ console.log("ERROR:"+event.filename+"("+event.lineno+"):"+event.message); };
建議在使用Web Worker時,始終都要使用onerror事件處理程序,即使這個函數除了把錯誤記錄到日誌中什麽也不做都可以。否則,Worker就會在發生錯誤時,悄無聲息地失敗了
【停止子線程】
任何時候,只要調用terminate()方法就可以停止Worker的工作。而且,Worker中的代碼會立即停止執行,後續的所有過程都不會再發生(包括error和message事件也不會再觸發)
worker.terminate();//立即停止 Worker 的工作
子線程
【作用域】
關於子線程Web Worker,最重要的是要知道它所執行的javascript代碼完全在另一個作用域中,與當前網頁中的代碼不共享作用域。在Web Worker中,同樣有一個全局對象和其他對象以及方法
Web Worker中的全局對象是worker對象本身。也就是說,在這個特殊的全局作用域中,this和self引用的都是worker對象。為便於處理數據,Web Worker本身也是一個最小化的運行環境,包括以下:
1、最小化的navigator對象,包括onLine、appName、appVersion、userAgent和platform屬性
2、只讀的location對象
3、setTimeout()、setInterval()、clearTimeout()和clearInterval()方法
4、XMLHttpReguest構造函數
顯然,Web Worker的運行環境與頁面環境相比,功能是相當有限的
【事件監聽】
當主線程在worker對象上調用postMessage()時,數據會以異步方式被傳遞給子線程worker,進而觸發子線程worker中的message事件。為了處理來自頁面的數據,同樣也需要創建一個onmessage事件處理程序
self代表子線程自身,self.addEventListener表示對子線程的message事件指定回調函數(直接指定onmessage屬性的值也可)。回調函數的參數是一個事件對象,它的data屬性包含主線程發來的信號
/* File: work.js */ self.addEventListener(‘message‘, function(e){ var data = event.data; //處理數據 }, false);
【傳遞消息】
子線程使用postMessage()方法向主線程傳遞信息
/* File: work.js */ self.onmessage = function(e) { var data = e.data; //處理數據 var method = data.method; var args = data.args; var reply = doSomething(args); self.postMessage({method: method, reply: reply}); };
【包含腳本】
無法在子線程Worker中動態創建新的<script>元素,但可以調用importScripts()方法來向Worker中添加其他腳本。這個方法接收一個或多個指向javascript文件的URL。每個加載過程都是異步進行的,因此所有腳本加載並執行之後,importScripts()才會執行
/* File: work.js */ importScripts("file1.js", "file2.js");
即使file2.js先於file1.js下載完,執行的時候仍然會按照先後順序執行。而且,這些腳本是在子線程Worker的全局作用域中執行,如果腳本中包含與頁面有關的javascript代碼,那麽腳本可能無法正確運行。請記住,Worker中的腳本一般都具有特殊的用途,不會像頁面中的腳本那麽功能寬泛
【停止子線程】
在子線程Worker中,調用close()方法也可以停止工作。就像在主線程中調用terminate()方法一樣,子線程Worker停止工作後就不會再有事件發生了
/* File: work.js */ self.close();
同頁面
通常情況下,子線程載入的是一個單獨的javascript文件,但是也可以載入與主線程在同一個網頁的代碼
假設網頁代碼如下:
<!DOCTYPE html> <body> <script id="worker" type="app/worker"> addEventListener(‘message‘, function() { postMessage(‘Im reading Tech.pro‘); }, false); </script> </body> </html>
我們可以讀取頁面中的script,用worker來處理
var blob = new Blob([document.querySelector(‘#worker‘).textContent]);
這裏需要把代碼當作二進制對象讀取,所以使用Blob接口。然後,這個二進制對象轉為URL,再通過這個URL創建worker
var url = window.URL.createObjectURL(blob); var worker = new Worker(url);
部署事件監聽代碼
worker.addEventListener(‘message‘, function(e) { console.log(e.data); }, false);
最後,啟動worker
worker.postMessage(‘‘);
整個頁面的代碼如下:
<!DOCTYPE html> <body> <script id="worker" type="app/worker"> addEventListener(‘message‘, function() { postMessage(‘Work done!‘); }, false); </script> <script> (function() { var blob = new Blob([document.querySelector(‘#worker‘).textContent]); var url = window.URL.createObjectURL(blob); var worker = new Worker(url); worker.addEventListener(‘message‘, function(e) { console.log(e.data); }, false); worker.postMessage(‘‘); })(); </script> </body> </html>
可以看到,主線程和子線程的代碼都在同一個網頁上面
應用
上面的段落介紹了如何使用Web Worker,那麽它到底有什麽用,可以幫我們解決那些實現問題呢?
Web Worker的一個優勢在於能夠執行處理器密集型的運算而不會阻塞UI線程,下面來看一個斐波那契(fibonacci)數列的例子
在數學上,fibonacci數列被以遞歸的方法定義:
F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)
javascript的常用實現為:
var fibonacci = function(n) { return n <2 ? n : arguments.callee(n - 1) + arguments.callee(n - 2); };
在chrome中用該方法進行40的fibonacci數列執行時間為13084.775毫秒,由於javascript是單線程執行的,在求數列的過程中瀏覽器不能執行其它javascript腳本,UI渲染線程也會被掛起,從而導致瀏覽器進入僵死狀態
<script> var fibonacci = function(n) { return n <2 ? n : arguments.callee(n - 1) + arguments.callee(n - 2); }; var t0 = window.performance.now(); fibonacci(40); var t1 = window.performance.now(); //fibonacci函數執行了13084.775毫秒 console.log("fibonacci函數執行了" + (t1 - t0) + "毫秒") </script>
使用web worker將數列的計算過程放入一個新線程裏去執行將避免這種情況的出現
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> <title>web worker fibonacci</title> </head> <body> <script> if(window.Worker){ var worker = new Worker(‘fibonacci.js‘); worker.onmessage = function(e){ var t1 = window.performance.now(); //fibonacci函數執行了11741.900000000001毫秒 console.log("fibonacci函數執行了" + (t1 - t0) + "毫秒") } worker.onerror = function(e){ console.log("ERROR:"+e.filename+"("+e.lineno+"):"+e.message); }; worker.postMessage(40); var t0 = window.performance.now(); } </script> </body> </html>
//fibonacci.js var fibonacci =function(n) { return n < 2 ? n : arguments.callee(n - 1) + arguments.callee(n - 2); }; self.onmessage = function(e) { self.postMessage(fibonacci(e.data)); };
雖然fibonacci數列的計算時間並沒有縮短多少,但由於其完全在自己獨立的線程中計算,只是在計算完成之後將結果發回主線程。所以並不會影響到主線程的代碼執行
因此,利用web worker我們可以在前端執行一些復雜的大量運算而不會影響頁面的展示
Web Worker