Web Worker

by Vivid 7. 十一月 2012 04:18

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號:N121113001
出刊日期:2012/11/7

有時網頁應用程式上的JavaScript會有愈執行愈慢的趨勢,有時會顯示「Slow Script」的訊息,可能的原因是JavaScript使用單一執行緒一次只執行一件事情,因執行中的程式需要花費較久的運算時間,在尚未執行完成之前,無法同時做其它事情,例如分身來回應使用者的互動。

HTML5支援Web Worker,可以在網頁程式上建立額外的執行緒,來執行花費時間較久的工作,以協助程式設計師建立能快速回應的網頁。

 

Web Worker

你可以在瀏覽器上建立一至多個Web Worker,每一個Web Worker要執行的程式碼定義在一個JavaScript檔案中。Web Worker不能夠存取DOM,如window、document以及parent物件或主程式上的變數以及函數等等,那些工作應該交代給執行網頁的UI執行緒來完成。

瀏覽器傳送訊息(Message)給Web Worker,以啟動Web Worker來執行。當Web Worker工作完畢,需要傳送訊息(Message)給瀏覽器,最後再由主執行緒來處理Web Worker運算完的結果,例如更新網頁中的DOM。

大部分的瀏覽器都有支援Web Worker,在開發階段,您可以造訪http://caniuse.com/#feat=webworkers 網頁來檢視不同的瀏覽器之支援程度,請參考下圖所示:

clip_image002

圖 1:支援Web Worker的瀏覽器。

不過在設計Web Worker時,最好先利用程式碼判斷瀏覽器是否提供支援,例如ie9瀏覽器就不支援Web Worker,但ie10瀏覽器就可以使用Web Worker。我們無法得知使用者會使用哪一個版本的瀏覽器來執行,因此建議在程式中進行判斷,並適當提示使用者,只要使用一小段JavaScript程式碼就可以判斷瀏覽器是否支援Web Worker,參考以下範例程式碼所示,在DOMContentLoaded事件中利用window["Worker"]語法來判斷,若瀏覽器支援Web Worker,window就會包含Worker屬性:

 

 

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv = "Content-Type" content = "text/html; charset=utf-8"/>
    <title> </title>
  <script>
    function contentLoaded () {
      var result = document.getElementById("result");
      if ( window["Worker"] ) {     
        result.innerHTML = "Support";
      }
      else {
        result.innerHTML = "No Support";
      }
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded , false );
  </script>
</head>
<body>
  <div id = "result"> </div>
</body>
</html>

 

此範例判斷瀏覽器是否支援Web Worker後,在<div>標籤上顯示是否支援的訊息,請參考下圖所示,此為Chrome瀏覽器執行的結果:

clip_image003

圖 2:為Chrome瀏覽器執行的結果。

 

設計Web Worker

接下來讓我們來了解一下,如何設計Web Worker, 我們將Web Worker要執行的程式碼定義在一個名為myWorker.js 的JavaScript檔案中:

 

onmessage = SayHI;
function SayHI( event ) {
  var name = event.data;
  postMessage( "hi," + name );
}

 

 

在myWorker.js檔案中,我們可以利用worker自己的onmessage屬性來攔截message事件,為方便說明,一開始我們讓範例儘量保持簡單,只要收到訊息觸發message事件,便執行SayHi方法,從event.data取得傳遞過來的資料,然後叫用postMessage方法,回傳hi的訊息。

 

使用Web Worker

那麼在網頁之中,如何使用Web Worker呢? 參考以下HTML5網頁,要使用Web Worker需要建立一個Worker物件,並傳入包含Worker程式碼所在的「myworker.js」檔名來進行初始化,JavaScript檔案所在的URL可以使用絕對或相對路徑的方式來傳遞。接著將Worker物件指定給一個名為worker的 JavaScript變數:

 

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title></title>
  <script>
    function contentLoaded () {
      var worker = new Worker( "myWorker.js" );
      worker.postMessage( "Mary" );
      worker.onmessage = function ( event ) {
        document.getElementById( "result" ).innerHTML = event.data;
      }
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded , false );
  </script>
</head>
<body>
  <div id="result">
  </div>
</body>
</html>

 

 

你可以利用相同的方式,建立兩個以上的Worker物件。若需要傳送訊息到Worker,這部分只要利用Worker的postMessage方法就可以達成,例如範例中傳送「Mary」字串到Worker。

接著在網頁之中便可以利用onmessage屬性來攔截message事件,以便在收到訊息傳送到來的事件處程式中,利用event.data取得worker執行完的結果,最後將執行結果顯示在畫面上。網頁執行的結果如下圖所示:

clip_image004

圖 3: Web Worker執行結果。

 

傳送不同型別參數給Web Worker

主程式除了可以傳送字串資料到Web Worker之外,也可以傳送不同其它類型的參數,例如以下的範例網頁傳送一個JSON物件,紀錄員工的id 與name到 Web Worker:

 

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title></title>
  <script>
    function contentLoaded () {
      var worker = new Worker( "myWorker03.js" );
      worker.postMessage( { id: "001", name: "mary" } );
      worker.onmessage = function ( event ) {
        document.getElementById( "result" ).innerHTML = event.data;
      }
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded, false );
  </script>
</head>
<body>
  <div id="result">
  </div>
</body>
</html>

 

 

 

而Worker同樣可以從message事件的event物件之event.data取出JSON物件:

 

self.onmessage = processData;
function processData( event ) { 
    var employee = event.data;
    postMessage( "Employee data :" + employee.id + "," + employee.name );
}

 

 

此範例執行結果如下圖所示:

clip_image005

圖 4:Worker可以接收JSON物件。

另一例子是傳送陣列的資料,例如下面程式中定義一個陣列存放興趣,並利用postMessage傳送到Worker:

 

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content="text/html; charset=utf-8" />
  <title> </title>
  <script>
    function contentLoaded () {
      var worker = new Worker( "myWorker04.js" );
      worker.postMessage( ["游泳", "爬山", "散步"] );
      worker.onmessage = function ( event ) {
        document.getElementById( "result" ).innerHTML = event.data;
      }
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded , false );
  </script>
</head>
<body>
  <div id = "result">
  </div>
</body>
</html>

 

在worker程式中,利用event物件取出陣列的資料,然後傳送訊息到主程式:

onmessage = processData;
function processData( event ) {
  var array = event.data;
  var result = "你的興趣是:" + array[0] + "," + array[1] + "," + array[2];
  postMessage( result );
}

 

此範例執行結果如下圖所示:

clip_image006

圖 5:Worker可以接收陣列。

 

建立多個Web Worker

我們在主程式中也可以建立多個Web Worker來協同運作。以下範例HTML網頁中,利用一個for迴圈,建立三個Worker物件,三個Worker物件都會執行myWorker05.js檔案中的程式。然後將建立的Worker物件利用陣列的push方法,加入名為workers的陣列之中:

 

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title> </title>
  <script>
    function contentLoaded () {
      var workers = [];
      var data = ["Mary", "Candy", "Lilly"];
      for ( var i = 0; i < 3; i++ ) {
        var worker = new Worker( "myWorker05.js" );
        workers.push( worker );
        worker.onmessage = function ( event ) {
          document.getElementById( "result" ).innerHTML += event.data;         
        }
      }
      for ( var i = 0; i < 3; i++ ) {
        worker.postMessage( data[i] );
      }
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded , false );
  </script>
</head>
<body>
  <div id = "result">
  </div>
</body>
</html>

 

最後利用一個迴圈,一一取出陣列中的worker物件,叫用postMessage方法傳送訊息。Worker程式碼如下:

 

onmessage = SayHI;
function SayHI( event ) {
  var name = event.data;
  postMessage( " hi," + name );
}

 

此範例執行結果如下圖所示:

clip_image007

圖 6:建立多個Web Worker物件執行。

 

 

使用importScripts匯入JavaScript

有時網站之中會將經常使用到的JavaScript程式碼,分門別類放到不同的檔案之中,然後在不同的網頁之中引用。因為Web Worker不能夠存取到DOM物件,因此若Web Worker需要引用其它JavaScript檔案中的程式碼,可以利用Web Worker新增的importScripts全域函數,一次匯入多個JavaScript檔案,使用逗號做區隔。舉例來說若有一個ims.js檔案內容如下,包含一個ProcessString方法:

 

function ProcessString( name ) {
  return " hi," + name
}

 

在Web Worker要執行的JavaScript檔案之中,就可以直接匯入,並叫用ProcessString方法:

 

importScripts( "ims.js" )
onmessage = SayHI;
function SayHI( event ) {
  var name = event.data;
  var result = ProcessString( name );
  postMessage(  result );
}

 

 

 

錯誤處理

Web Worker執行若產生例外錯誤,將會觸發Web Worker物件的error事件,我們可以在網頁之中,攔截這個錯誤。只需要註冊error事件,事件發生時,從事件的message屬性可以取得錯誤訊息,例如以下範例程式所示:

 

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title></title>
  <script>
    function contentLoaded () {
      var worker = new Worker( "myErrorWorker.js" );
      worker.postMessage( "Mary" );
      worker.onerror = function ( e ) {
        console.log( "發生錯誤:" + e.message );
      }
      worker.onmessage = function ( event ) {
        document.getElementById( "result" ).innerHTML = event.data;
      }
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded , false );
  </script>
</head>
<body>
  <div id = "result">
  </div>
</body>
</html>

 

 

為模擬錯誤,myErrorWorker.js檔案內容如下,利用JavaScript的throw關鍵字,丟出一個例外錯誤:

 

onmessage = SayHI;
function SayHI( event ) {
  throw "unknown error!!";
}

 

 

以下是Chrome瀏覽器主控台印出的錯誤訊息,請參考下圖所示:

clip_image008

圖 7:Chrome瀏覽器主控台印出的錯誤訊息。

 

錯誤處理工具

Chrome瀏覽器提供除錯Web Worker的能力,我們可以在Chrome瀏覽器開發人員工具中,勾選Workers區塊的「Pause on start」選項,請參考下圖所示:

clip_image010

圖 8:Chrome瀏覽器Web Worker除錯工具。

Worker執行時就會跳出除錯視窗,參考下圖所示:

clip_image012

圖 9:Chrome瀏覽器Web Worker除錯工具。

另外一個好用的除錯工具就是Visual Studio 2012搭配IE10瀏覽器,你可以直接在worker程式中設中斷點 ,請參考下圖所示:

clip_image013

圖 10:在Visual Studio 2012設定中斷點。

然後使用Ie10瀏覽器,按F5來除錯,請參考下圖所示:

clip_image014

圖 11:使用Ie10瀏覽器除錯。

只要執行遇到中斷點,便可以利用Visual Studio 2012工具的功能來協助除錯請參考下圖所示:

clip_image015

圖 12:Visual Studio 2012除錯工具。

 

Shared Worker

還有另外一種類型的Worker,稱Shared Worker。不過支援的瀏覽器比較少一些,支援的瀏覽器列表可以從http://caniuse.com/#feat=webworkers 網站查詢,請參考下圖所示:

clip_image017

圖 13:支援Shared Worker的瀏覽器。

Shared Worker和一般的Worker一樣,在背景執行,不同點是可以跨同一個網站中不同的頁面(Page)或頁籤(Tab)來存取。因為Shared Worker可以同時讓多個用戶端連結,因此利用port來識別,以及傳送訊息。我們直接來看一個範例,以下範例程式碼,建立SharedWorker物件,利用port. postMessage來送訊息;其它部分的程式和使用Web Worker差不多:

 

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv = "Content-Type" content = "text/html; charset=utf-8" />
  <title></title>
  <script>
    function contentLoaded() {
      var worker = new SharedWorker( "myShareWorker.js" );
      worker.port.postMessage( "Mary" );
      worker.port.onmessage = function ( event ) {
        document.getElementById( "result" ).innerHTML = event.data;
      }
    }
    window.addEventListener( "DOMContentLoaded", contentLoaded , false );
  </script>
</head>
<body>
  <div id = "result">
  </div>
</body>
</html>

myShareWorker.js程式如下,程式寫法也和Web Worker差不多,不過這回我們攔截了connect事件,當用戶端的執行緒連結到Shared Worker時,會自動觸發此事件。我們在事件處理常式中,使用port.onmessage註冊message事件,然後利用port.postMessage傳送訊息:

var connections = 0;
onconnect = processData;
function processData( event ) {     
    var port = event.ports[0];
    connections++;
    port.onmessage = function (e) {
        port.postMessage("hi , " + e.data + " ,Conntctions :" + connections);
    }
  }

 

 

最後開啟兩個Chrome瀏覽器來測試執行,參考下圖的執行結果:

clip_image018

圖 14:Shared Worker執行結果。

總結

JavaScript預設是使用單一執行緒來執行程式碼,因此當你要執行耗時的程式碼時,便會阻礙UI執行緒處理其它事情,例如點選按鈕,在文字方塊輸入資料的動作。HTML5 提供Web Worker可以使用另一條執行緒在背後為你執行耗時的程式碼。

Web Worker不能直接更新DOM的主要理由是,避免多條執行緒同時更新DOM物件內容,可能會造成資料錯亂問題。

Tags:

.NET Magazine國際中文電子雜誌 | HTML | 許薰尹Vivid Hsu

不允許評論

NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List