Web Workers 的基本信息


問題:JavaScript 並行性

要將有趣的應用(例如從側重服務器端的實施)移植到客戶端 JavaScript,存在很多制約瓶頸。其中包括瀏覽器兼容性、靜態類型、可訪問性和性能。幸運的是,隨着瀏覽器供應商快速提高 JavaScript 引擎的速度,性能已不再是瓶頸。

仍在阻礙 JavaScript 的實際上是語言本身。JavaScript 屬於單線程環境,也就是說無法同時運行多個腳本。例如,假設有一個網站,它需要處理 UI 事件,查詢並處理大量 API 數據以及操作 DOM。這很常見,不是嗎?遺憾的是,由於受到瀏覽器 JavaScript 運行時的限制,所有這些操作都無法同時進行。腳本是在單個線程中執行的。

開發人員會使用 setTimeout()setInterval()XMLHttpRequest 和事件處理程序等技術模擬“並行”。所有這些功能確實都是異步運行的,但沒有阻礙未必就意味着並行。系統會在生成當前執行腳本后處理異步事件。好消息是,HTML5 為我們提供了優於這些技巧的技術。

Web Worker 簡介:為 JavaScript 引入線程技術

Web Worker 規范定義了在網絡應用中生成背景腳本的 API。您可以通過 Web Worker 執行一些操作,例如觸發長時間運行的腳本以處理計算密集型任務,同時卻不會阻礙 UI 或其他腳本處理用戶互動。這有助於解決令人討厭的“無響應腳本”對話框(大家都有些愛上它了吧)問題:

無響應腳本對話框 常見無響應腳本對話框。

Worker 利用類似線程的消息傳遞實現並行。這非常適合您確保對 UI 的刷新、性能以及對用戶的響應。

Web Worker 的類型

值得注意的是,規范中介紹了兩種 Web Worker:專用 Worker共用 Worker。本文只涉及專用 Worker,並在全文中將其稱為“Web Worker”或“Worker”。

使用入門

Web Worker 在獨立線程中運行。因此,它們執行的代碼需要保存在一個單獨的文件中。但在保存代碼前,我們要先在您的主網頁上創建新的 Worker 對象。構造函數采用 Worker 腳本的名稱:

var worker = new Worker('task.js');

如果指定的異步下載文件存在,瀏覽器就會生成新的 Worker 線程。在完全下載並執行文件之前,系統不會生成 Worker。如果指向您 Worker 的路徑返回 404,Worker 就會在不顯示任何提示的情況下失敗。

創建 Worker 之后,通過調用 postMessage() 方法啟動:

worker.postMessage(); // Start the worker.
通過消息傳遞與 Worker 通信

Worker 與其父網頁之間的通信是通過事件模型和 postMessage() 方法實現的。postMessage() 可以接受字符串或 JSON 對象作為單個參數,具體取決於您的瀏覽器/版本。新式瀏覽器的最新版支持傳遞 JSON 對象。

以下示例使用字符串將“Hello World”傳遞給了 doWork.js 中的 Worker。Worker 直接返回了傳遞給它的消息。

主腳本:

var worker = new Worker('doWork.js');

worker.addEventListener('message', function(e) {
  console.log('Worker said: ', e.data);
}, false);

worker.postMessage('Hello World'); // Send data to our worker.
doWork.js (Worker):

self.addEventListener('message', function(e) {
  self.postMessage(e.data);
}, false);
 
        

在主網頁中調用 postMessage() 時,我們的 Worker 通過定義 message 事件的 onmessage 處理程序來處理消息。您可以在 Event.data 中訪問消息有效負載(此示例中為“Hello World”)。雖然這個特殊的示例並不精彩,但它說明 postMessage() 也是您將數據傳回主線程的一種方法。很方便!

在主網頁和 Worker 之間傳遞的消息是復制而不是共享的。例如,下一示例中 JSON 消息的“msg”屬性在兩個位置中均可訪問。即使對象運行在單獨的專用空間中,系統似乎也會將其直接傳遞給 Worker。實際發生的情況是,系統將對象傳遞給 Worker 后,會將其序列化,隨后在另一端解取消序列化。由於網頁和 Worker 並不共享同一實例,因此每次傳遞時都要進行復制。大部分瀏覽器通過在任一端上對值進行自動 JSON 編碼/解碼來實施此功能。

下面是一個使用 JSON 對象傳遞消息的更復雜的示例。

主腳本:

<button onclick="sayHI()">Say HI</button>
<button onclick="unknownCmd()">Send unknown command</button>
<button onclick="stop()">Stop worker</button>
<output id="result"></output>

<script>
  function sayHI() {
    worker.postMessage({'cmd': 'start', 'msg': 'Hi'});
  }

  function stop() {
    // Calling worker.terminate() from this script would also stop the worker.
    worker.postMessage({'cmd': 'stop', 'msg': 'Bye'});
  }

  function unknownCmd() {
    worker.postMessage({'cmd': 'foobard', 'msg': '???'});
  }

  var worker = new Worker('doWork2.js');

  worker.addEventListener('message', function(e) {
    document.getElementById('result').textContent = e.data;
  }, false);
</script>

doWork2.js:

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 + '. (buttons will no longer work)');
      self.close(); // Terminates the worker.
      break;
    default:
      self.postMessage('Unknown command: ' + data.msg);
  };
}, false);

請注意:停止 Worker 的方法有兩種:在主網頁中調用 worker terminate(),或在 Worker 本身內部調用 self.close()

示例:運行此 Worker!

打招呼發送未知命令停止 Worker

Worker 環境

Worker 作用域

就 Worker 來說,selfthis 指的都是 Worker 的全局作用域。因此,上一示例也可以寫成:

addEventListener('message', function(e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
  ...
}, false);

或者,您可以直接設置 onmessage 事件處理程序(雖然 JavaScript 高手們總是會推薦 addEventListener)。

onmessage = function(e) {
  var data = e.data;
  ...
};
適用於 Worker 的功能

由於 Web Worker 的多線程行為,所以它們只能使用 JavaScript 功能的子集:

  • navigator 對象
  • location 對象(只讀)
  • XMLHttpRequest
  • setTimeout()/clearTimeout()setInterval()/clearInterval()
  • 應用緩存
  • 使用 importScripts() 方法導入外部腳本
  • 生成其他 Web Worker

Worker 無法使用:

  • DOM(非線程安全)
  • window 對象
  • document 對象
  • parent 對象
加載外部腳本

您可以通過 importScripts() 函數將外部腳本文件或庫加載到 Worker 中。該方法采用零個或多個字符串表示要導入的資源的文件名。

此示例將 script1.jsscript2.js 加載到了 Worker 中:

worker.js:

importScripts('script1.js');
importScripts('script2.js');

也可以寫成單個導入語句:

importScripts('script1.js', 'script2.js');
子 Worker

Worker 可以生成子 Worker。這對於在運行時進一步拆分大任務來說非常重要。但是,子 Worker 還有幾點注意事項:

  • 子 Worker 必須托管在與父網頁相同的來源中。
  • 子 Worker 中的 URI 應相對於父 Worker 的位置進行解析(與主網頁不同)。

請注意,大部分瀏覽器會為每個 Worker 生成單獨的進程。在您開始生成 Worker 場之前,請注意不要占用太多的用戶系統資源。這樣做的一個原因是,在主網頁和 Worker 之間傳遞的消息是復制而不是共享的。請參閱通過消息傳遞與 Worker 通信

有關子 Worker 生成方法的示例,請參閱規范中的相關示例

內嵌 Worker

如果您想即時創建 Worker 腳本,或者在不創建單獨 Worker 文件的情況下創建獨立網頁,那該怎么做呢?在新 BlobBuilder 界面中,您可以創建 BlobBuilder 並以字符串形式附上 Worker 代碼,從而在與主邏輯相同的 HTML 文件中“內嵌”Worker:

// Prefixed in Webkit, Chrome 12, and FF6: window.WebKitBlobBuilder, window.MozBlobBuilder
var bb = new BlobBuilder();
bb.append("onmessage = function(e) { postMessage('msg from worker'); }");

// Obtain a blob URL reference to our worker 'file'.
// Note: window.webkitURL.createObjectURL() in Chrome 10+.
var blobURL = window.URL.createObjectURL(bb.getBlob());

var worker = new Worker(blobURL);
worker.onmessage = function(e) {
  // e.data == 'msg from worker'
};
worker.postMessage(); // Start the worker.
Blob 網址

window.URL.createObjectURL() 的調用十分奇妙。此方法創建了一個簡單的網址字符串,該字符串可用於 DOM FileBlob 對象中存儲的參考數據。例如:

blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

Blob 網址是唯一的,且只要應用存在,該網址就會一直有效(例如直到卸載 document 為止)。如果您要創建很多 Blob 網址,最好發布不再需要的參考資料。您可以通過將 Blob 網址傳遞給 window.URL.revokeObjectURL() 來明確發布該網址:

window.URL.revokeObjectURL(blobURL); // window.webkitURL.createObjectURL() in Chrome 10+.

在 Chrome 瀏覽器中,有一個很實用的頁面可供您查看創建的所有 Blob 網址:chrome://blob-internals/

完整示例

再進行一個步驟,我們就會清楚如何將 Worker 的 JavaScript 代碼內嵌在網頁中了。此技術使用 <script> 標簽定義 Worker:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>

  <div id="log"></div>

  <script id="worker1" type="javascript/worker">
    // This script won't be parsed by JS engines because its type is javascript/worker.
    self.onmessage = function(e) {
      self.postMessage('msg from worker');
    };
    // Rest of your worker code goes here.
  </script>

  <script>
    function log(msg) {
      // Use a fragment: browser will only render/reflow once.
      var fragment = document.createDocumentFragment();
      fragment.appendChild(document.createTextNode(msg));
      fragment.appendChild(document.createElement('br'));

      document.querySelector("#log").appendChild(fragment);
    }

    var bb = new BlobBuilder();
    bb.append(document.querySelector('#worker1').textContent);

    // Note: window.webkitURL.createObjectURL() in Chrome 10+.
    var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
    worker.onmessage = function(e) {
      log("Received: " + e.data);
    }
    worker.postMessage(); // Start the worker.
  </script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
</head>
<body>

  <div id="log"></div>

  <script id="worker1" type="javascript/worker">
    // This script won't be parsed by JS engines because its type is javascript/worker.
    self.onmessage = function(e) {
      self.postMessage('msg from worker');
    };
    // Rest of your worker code goes here.
  </script>

  <script>
    function log(msg) {
      // Use a fragment: browser will only render/reflow once.
      var fragment = document.createDocumentFragment();
      fragment.appendChild(document.createTextNode(msg));
      fragment.appendChild(document.createElement('br'));

      document.querySelector("#log").appendChild(fragment);
    }

    var bb = new BlobBuilder();
    bb.append(document.querySelector('#worker1').textContent);

    // Note: window.webkitURL.createObjectURL() in Chrome 10+.
    var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
    worker.onmessage = function(e) {
      log("Received: " + e.data);
    }
    worker.postMessage(); // Start the worker.
  </script>
</body>
</html>

在我看來,這種新方法稍顯清晰明了。它通過 id="worker1"type='javascript/worker' 定義腳本標記(這樣瀏覽器就不會解析 JavaScript 了)。系統會使用 document.querySelector('#worker1').textContent 以字符串形式提取該代碼並將其傳遞給 BlobBu lder.append()

加載外部腳本

在使用這些技術內嵌 Worker 代碼時,importScripts() 只會在您提供絕對 URI 的情況下生效。如果您嘗試傳遞相對 URI,瀏覽器就會提示出現安全錯誤。原因:系統會通過 blob: 前綴解析 Worker(現在通過 Blob 網址創建),而您的應用會通過其他(可能為 http://)方案運行。因此,失敗原因在於跨源限制。

在內嵌 Worker 中利用 importScripts() 的一種方法是,通過將相關網址傳遞給內嵌 Worker 並手動構建絕對網址來“導入”運行您主腳本的當前網址。這可以確保外部腳本是從同一來源導入的。假設您的主應用是在 http://example.com/index.html 上運行的:

...
<script id="worker2" type="javascript/worker">
self.onmessage = function(e) {
  var data = e.data;

  if (data.url) {
    var url = data.url.href;
    var index = url.indexOf('index.html');
    if (index != -1) {
      url = url.substring(0, index);
    }
    importScripts(url + 'engine.js');
  }
  ...
};
</script>
<script>
  var worker = new Worker(window.URL.createObjectURL(bb.getBlob()));
  worker.postMessage({url: document.location});
</script>

處理錯誤

與任何 JavaScript 邏輯一樣,您需要處理 Web Worker 中出現的任何錯誤。如果在執行 Worker 時出現錯誤,就會觸發 ErrorEvent。相關界面中包含用於找出錯誤內容的三個實用屬性:filename - 導致錯誤的 Worker 腳本的名稱;lineno - 出現錯誤的行號;以及 message - 有關錯誤的實用說明。以下示例設置了 onerror 事件處理程序以便打印錯誤內容:

<output id="error" style="color: red;"></output>
<output id="result"></output>

<script>
  function onError(e) {
    document.getElementById('error').textContent = [
      'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message].join('');
  }

  function onMsg(e) {
    document.getElementById('result').textContent = e.data;
  }

  var worker = new Worker('workerWithError.js');
  worker.addEventListener('message', onMsg, false);
  worker.addEventListener('error', onError, false);
  worker.postMessage(); // Start worker without a message.
</script>

示例:workerWithError.js 嘗試執行 1/x,其中 x 未定義。

運行

workerWithError.js:

self.addEventListener('message', function(e) {
  postMessage(1/x); // Intentional error.
};

安全說明

本地訪問限制

由於 Google Chrome 瀏覽器的安全限制,Worker 無法在最新版瀏覽器中本地運行(例如通過 file://),且會在不顯示任何提示的情況下失敗!要通過 file:// 方案運行您的應用,請使用 --allow-file-access-from-files 標記設置來運行 Chrome 瀏覽器。請注意:不推薦使用此標記設置來運行您的主瀏覽器。此標記設置僅供測試用,請勿用於常規瀏覽。

其他瀏覽器不存在相同的限制。

同源注意事項

Worker 腳本必須是將相同方案作為調用網頁的外部文件。因此,您無法通過 data: 網址或 javascript: 網址加載腳本,且 https: 網頁無法啟動以 http: 網址開頭的 Worker 腳本。

來源:http://www.html5rocks.com/zh/tutorials/workers/basics/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM