Web Workers - (Worker(專有) and SharedWorker(共享))

  • Web Worker爲Web內容在後臺線程中運行腳本提供了一種簡單的方法
  • 線程能夠執行任務而不干擾用戶界面
  • 可使用XMLHttpRequest執行 I/O (儘管responseXML和channel屬性老是爲空)
  • 一個worker 能夠將消息發送到建立它的JavaScript代碼, 經過將消息發佈到該代碼指定的事件處理程序

——————————————————————————————————————————

Web Workers API

  • 一個worker是使用一個構造函數建立的一個對象(e.g. Worker()) 運行一個命名的JavaScript文件
  • workers 運行在另外一個全局上下文中,不一樣於當前的window
    • 使用 window快捷方式獲取當前全局的範圍 (而不是self) 在一個 Worker 內將返回錯誤。
  • DedicatedWorkerGlobalScope 對象表明了專用worker的上下文
    • 專用workers是指標準worker僅在單一腳本中被使用,一個專用worker僅僅能被首次生成它的腳本使用
  • 共享worker的上下文是SharedWorkerGlobalScope對象,共享worker能夠同時被多個腳本使用。
  • 在worker內,不能直接操做DOM節點,也不能使用window對象的默認方法和屬性。
  • 可使用包括WebSockets,IndexedDB等數據存儲機制。參考https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Functions_and_classes_available_to_workers
  • workers和主線程間的數據傳遞經過這樣的消息機制進行——雙方都使用postMessage()方法發送各自的消息,使用onmessage事件處理函數來響應消息(消息被包含在Message事件的data屬性中)。
    • 這個過程當中數據並非被共享而是被複制。
  • 只要運行在同源的父頁面中,workers能夠依次生成新的workers

——————————————————————————————————————————

專用worker

worker特性檢測

  • 爲了更好的錯誤處理控制以及向下兼容,將你的worker運行代碼包裹在如下代碼中是一個很好的想法
if (window.Worker) { ... }

生成一個專用worker

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

專用worker中消息的接收和發送

  • 在worker內部,worker是有效的全局做用域。
// 主線程中
// 發送
first.onchange = function() { // first是一個input元素
  myWorker.postMessage([first.value]);
  console.log('Message posted to worker');
}
// 接收
myWorker.onmessage = function(e) {
  result.textContent = e.data; // result是個p元素
  console.log('Message received from worker');
}

// worker.js 中
// 接收和發送
onmessage = function(e) {
  console.log('Message received from main script');
  var workerResult = 'Result: ' + (e.data[0] * 10);
  console.log('Posting message back to main script');
  postMessage(workerResult);
}

終止worker

  • worker 線程會被當即殺死,不會有任何機會讓它完成本身的操做或清理工做。
// 主線程中
myWorker.terminate();
  • 而在worker線程中,workers 也能夠調用本身的 close 方法進行關閉
close();

處理錯誤

  • 當 worker 出現運行中錯誤時,它的 onerror 事件處理函數會被調用
  • 會收到一個擴展了 ErrorEvent 接口的名爲 error的事件
  • 該事件不會冒泡但能夠被取消
  • 能夠調用錯誤事件的 preventDefault()方法,防止觸發默認動做
  • 錯誤事件有如下三個用戶關心的字段
    • message 錯誤消息
    • filename 發生錯誤的腳本文件名
    • lineno 發生錯誤時所在腳本文件的行號。

生成subworker

  • worker 可以生成更多的 worker。這就是所謂的subworker
  • 必須託管在同源的父頁面內
  • subworker 解析 URI 時會相對於父 worker 的地址而不是自身頁面的地址

引入腳本與庫

  • Worker 線程可以訪問一個全局函數importScripts()來引入腳本,該函數接受0個或者多個URI做爲參數來引入資源
  • 腳本路徑相對於建立當前Worker的window的域開始
importScripts();                        /* 什麼都不引入 */
importScripts('./foo.js');                /* 只引入 "foo.js" */
importScripts('foo.js', 'bar.js');      /* 引入兩個腳本 */
  • 若是腳本沒法加載,將拋出 NETWORK_ERROR 異常
  • importScripts() 以後的函數聲明依然會被保留,由於它們始終會在其餘代碼以前運行。
  • 腳本的下載順序不固定,但執行時會按照傳入 importScripts() 中的文件名順序進行。這個過程是同步完成的;直到全部腳本都下載並運行完畢,importScripts() 纔會返回。

——————————————————————————————————————————

共享worker

  • 一個共享worker能夠被多個腳本使用——即便這些腳本正在被不一樣的window、iframe或者worker訪問。
  • 若是共享worker能夠被多個瀏覽上下文調用,全部這些瀏覽上下文必須屬於同源(相同的協議,主機和端口號)。

生成一個共享worker

var myWorker = new SharedWorker('worker.js');
  • 一個共享worker通訊必須經過端口對象——一個確切的打開的端口供腳本與worker通訊(在專用worker中這一部分是隱式進行的)。
  • 在傳遞消息以前,端口鏈接必須被顯式的打開,打開方式是使用onmessage事件處理函數或者start()方法。
  • start()方法的調用只在一種狀況下須要,那就是消息事件被addEventListener()方法使用。
// 在主線程中

// 直接給port綁定事件函數,不須要start方法
  myWorker.port.onmessage = function(e) { 
    result1.textContent = e.data;
    console.log('Message received from worker');
    console.log(e.lastEventId);
  }

// 使用start方法顯示打開端口
myWorker.port.start();  // 父級線程中的調用
  • onconnect事件在父級線程中,設置onmessage事件處理函數,或者顯式調用start()方法時觸發
// 在Worker中

onconnect = function(e) { // onconnect 已連接事件
  var port = e.ports[0]; // 獲取端口對象

  // 直接給port綁定事件函數,不須要start方法
  port.onmessage = function(e) {
    var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
    port.postMessage(workerResult);
  }

  // 使用start方法顯示打開端口
  port.start(); // worker線程中的調用, 假設port變量表明一個端口
}

共享worker中消息的接收和發送

  • postMessage() 方法必須被端口對象調用
squareNumber.onchange = function() { // squareNumber是一個input元素
  myWorker.port.postMessage([squareNumber.value]);
  console.log('Message posted to worker');
}

——————————————————————————----——————

關於線程安全

  • 須要經過序列化對象來與線程交互特定的數據(好像postMessage已經自身實現了這個功能,大部分瀏覽器使用結構化拷貝來實現該特性。)

——————————————————————————————————————————

內容安全策略

  • 若是document使用了內容安全策略頭部Content-Security-Policy: script-src 'self'
    • 會禁止它內部包含的腳本代碼使用eval()方法。
    • 然而,若是腳本代碼建立了一個worker,在worker上下文中執行的代碼倒是可使用eval()的。
    • 爲了給worker指定內容安全策略,必須爲發送worker代碼的請求自己加上一個 內容安全策略。
  • worker腳本的源若是是一個全局性的惟一的標識符(例如,它的URL指定了數據模式或者blob),worker則會繼承建立它的document或者worker的CSP(Content security policy內容安全策略)。

——————————————————————————————————————————

worker中數據的接收與發送:詳細介紹

  • 拷貝而並不是共享的那個值稱爲 消息
  • 結構化拷貝算法能夠接收JSON數據以及一些JSON不能表示的數據——好比循環引用。

傳遞數據的例子

  • 通用異步 eval() 能夠繞開主進程的內容安全策略
// 主進程中
var asyncEval = (function () {
  // 用數組保存回調函數,用id標明對應code須要調用的執行函數在數組中的位置,應爲postMseeage是異步的
  var aListeners = []

  // 它的URL指定了數據模式或者blob
  var 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]; // 這種方式會致使數組長度不斷延長,並不理想
  };

  // sCode:須要經過eval執行的字符串型代碼
  // fListener:代碼執行結果的處理函數
  return function (sCode, fListener) { 
    aListeners.push(fListener || null);
    oParser.postMessage({
      "id": aListeners.length - 1,
      "code": sCode
    });
  };

})();

// Worker中(data URL 至關於一個網絡請求,它有以下返回:)
onmessage = function(oEvent) {
  postMessage({
    'id': oEvent.data.id,
    'evaluated': eval(oEvent.data.code)
  });
}

// 主進程中運用
asyncEval("\"Hello World!!!\"", function (sHTML) {
    document.body.appendChild(document.createTextNode(sHTML));
});

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})()");
  • 封裝一個Worker對象,方便爲Worker中每一個處理方法定義回調函數
  • 能夠指定調用內部的任意方法,並知足這些方法的傳參需求
// 主進程
var oMyTask = new QueryableWorker("my_task.js");
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 + " :-)");
});

// sURL Worker的運行腳本地址
// fDefListener onmessage的默認回調函數
// fOnError Worker的錯誤回調函數
function QueryableWorker(sURL, fDefListener, fOnError) {
  var oInstance = this
  var oWorker = new Worker(sURL)
  var oListeners = {};
  this.defaultListener = fDefListener || function () { };

  oWorker.onmessage = function (oEvent) {
    // rnb93qh 內部定義的參數key,內部處理方法處理好數據後回傳給主進程的參數
    // vo42t30 內部定義的方法key,內部處理方法定義好的主進程處理方法,須要經過addListener添加
    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; }

  // 調用Worker中定義好的方法,每一個方法都有對應的回調名,調用前須要先用addListener添加對應的回調函數
  // 參數1:內部已有的方法名
  // 參數2-n:傳給該方法的參數,能夠有多個
  // 經過私有key bk4e1h0和ktp3fm1來肯定內部定義好的方法
  this.sendQuery = function () {
    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.addListener = function (sName, fListener) {
    oListeners[sName] = fListener;
  };
  // 移除信息處理方法
  this.removeListener = function (sName) {
    delete oListeners[sName];
  };

  // 對象的一個方法用來發送信息,主要爲了區分sendQuery,用來調用Worker中的默認處理方法
  this.postMessage = function (vMsg) {
    Worker.prototype.postMessage.call(oWorker, vMsg);
  };
  // 用來銷燬Worker
  this.terminate = function () {
    Worker.prototype.terminate.call(oWorker);
  };
};


// worker中
// worker中的處理方法集合和對應的主進程回調方法
var queryableFunctions = {
  getDifference: function (nMinuend, nSubtrahend) {
    reply("printSomething", nMinuend - nSubtrahend);
  },
  waitSomething: function () {
    setTimeout(function () { reply("alertSomething", 3, "seconds"); }, 3000);
  }
};
// 處理信息的默認方法
function defaultQuery(vMsg) {
}

// 回傳信息給主進程
// vo42t30 標明主進程回調處理函數名
// rnb93qh 標明信息數組
function reply() {
  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")) {
    // self 是Worker中的常量,指向當前上下文
    queryableFunctions[oEvent.data.bk4e1h0].apply(self, oEvent.data.ktp3fm1);
  } else {
    defaultQuery(oEvent.data);
  }
};

經過轉讓全部權(可轉讓對象)來傳遞數據

  • Google Chrome 17 與 Firefox 18 包含另外一種性能更高的方法來將特定類型的對象(可轉讓對象)
  • 可轉讓對象從一個上下文轉移到另外一個上下文而不會通過任何拷貝操做。這意味着當傳遞大數據時會得到極大的性能提高。
  • 一旦對象轉讓,那麼它在原來上下文的那個版本將不復存在。該對象的全部權被轉讓到新的上下文內。

——————————————————————————----——————

嵌入式 worker

  • 一個 <script> 元素沒有 src 特性,而且它的 type 特性沒有指定成一個可運行的 mime-type,那麼它就會被認爲是一個數據塊元素,而且可以被 JavaScript 使用。
<script type="text/js-worker">
  // 該腳本不會被 JS 引擎解析,由於它的 mime-type 是 text/js-worker。
  var myVar = "Hello World!";
  onmessage = function (oEvent) {
    postMessage(myVar);
  };
  // 剩下的 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);
  }
  // 在過去...:
  // 咱們使用 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>
  • 將一個函數轉換爲blob,而後爲這個blob生成URL對象
function fn2workerURL(fn) {
  var blob = new Blob(['('+fn.toString()+')()'], {type: 'application/javascript'})
  return URL.createObjectURL(blob)
}

——————————————————————————————————————————

更多示例

在後臺執行運算

  • worker 的一個優點在於可以執行處理器密集型的運算而不會阻塞 UI 線程。
  • 實現傳入一個斐波那契數位置,返回該位置的對應值
  • 經過遞歸建立線程,來獲取改位置最後由多少個1構成來實現。(該例子至關消耗性能,慎用)
// worker執行腳本中,文件名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);
  }
};


// 主進程中
var worker = new Worker("fibonacci.js");

worker.onmessage = function (event) {
  document.getElementById("result").textContent = event.data;
};

worker.onerror = function (error) {
  throw error;
};

worker.postMessage(5);

劃分任務給多個 worker

  • 當多核系統流行開來,將複雜的運算任務分配給多個 worker 來運行已經變得十分有用,這些 worker 會在多處理器內核上運行這些任務。

——————————————————————————————————————————

其它類型的worker

  • ServiceWorkers (服務worker)通常做爲web應用程序、瀏覽器和網絡(若是可用)以前的代理服務器。它們旨在(除開其餘方面)建立有效的離線體驗,攔截網絡請求,以及根據網絡是否可用採起合適的行動並更新駐留在服務器上的資源。他們還將容許訪問推送通知和後臺同步API。
  • Audio Workers (音頻worker)使得在web worker上下文中直接完成腳本化音頻處理成爲可能。

——————————————————————————————————————————

worker中可用的函數和接口

  • Navigator 用戶代理的狀態和標識
  • XMLHttpRequest
  • Array, Date, Math, and String
  • WindowTimers.setTimeout and WindowTimers.setInterval
相關文章
相關標籤/搜索