[PHP] 使用 Web Workers

发表于:2个月前  阅读量:16655

摘要

专用 Web Worker (Dedicated Web Worker) 提供了一个简单的方法使得 web 内容能够在后台运行脚本。一旦 worker 创建后,它可以向由它的创建者指定的事件监听函数传递消息,这样该 worker 生成的所有任务就都会接收到这些消息。

专用 Web Worker (Dedicated Web Worker) 提供了一个简单的方法使得 web 内容能够在后台运行脚本。一旦 worker 创建后,它可以向由它的创建者指定的事件监听函数传递消息,这样该 worker 生成的所有任务就都会接收到这些消息。

worker 线程能够在不干扰 UI 的情况下执行任务。另外,它还能够使用 XMLHttpRequest (虽然 responseXML 与 channel 两个属性值始终是 null)来执行  I/O 操作。

查看 worker 的参考文档 Worker;本文通过提供例子和细节补全了前面的文档。提供给 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 (i.e. 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]);

要了解更多关于可转让对象的信息,请查看 HTML5Rocks 。

生成subworker

如果需要的话 Worker 能够生成更多的 Worker。这样的被称为 subworker,它们必须托管在与父页面相同的源内。同理,subworker 解析 URI 时会相对于父 worker 的地址而不是自身的页面。这使得 worker 容易监控它们的依赖关系。

Chrome 目前并不支持subworker。参见 crbug.com/31666 。

嵌入式 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 线程周期性而并非不间断的运行代码。

查看 setTimeout(), clearTimeout() setInterval() 与 clearInterval() 了解详情。还可以查看: JavaScript 定时器

终止 worker

如果你想立即终止一个运行中的 worker,可以调用 worker 的 terminate()方法:

myWorker.terminate();

worker 线程会被立即杀死,不会留下任何机会让它完成自己的操作或清理工作。

Workers 也可以调用自己的 nsIWorkerScope.close() 方法来关闭自己:

self.close();

处理错误

当 worker 出现运行时错误时,它的 onerror 事件处理函数会被调用。它会收到一个实现了 ErrorEvent 接口名为 error的事件。该事件不会冒泡,并且可以被取消;为了防止触发默认动作,worker 可以调用错误事件的 preventDefault() 方法。

错误事件拥有下列三个它感兴趣的字段:

  • message

  • 可读性良好的错误消息。

  • filename

  • 发生错误的脚本文件名。

  • lineno

  • 发生错误时所在脚本文件的行号。

访问 navigator 对象

Workers 可以在它的作用域内访问 navigator 对象。它含有如下能够识别浏览器的字符串,就像在普通脚本中做的那样:

  • appName

  • appVersion

  • platform

  • userAgent

引入脚本与库

Worker 线程能够访问一个全局函数,importScripts() ,该函数允许 worker 将脚本或库引入自己的作用域内。你可以不传入参数,或传入多个脚本的 URI 来引入;以下的例子都是合法的:

importScripts();                        /* 什么都不引入 */importScripts('foo.js');                /* 只引入 "foo.js" */importScripts('foo.js', 'bar.js');      /* 引入两个脚本 */

浏览器将列出的脚本加载并运行。每个脚本中的全局对象都能够被 worker 使用。如果脚本无法加载,将抛出 NETWORK_ERROR 异常,接下来的代码也无法执行。而之前执行的代码(包括使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Window/setTimeout" title="在指定的延迟时间之后调用一个函数或执行一个代码片段." style="margin: 0px; padding: 0px; border: 0px; color: rgb(0, 149,

关键词: HTML5
渝ICP备16002246号 Copyright © 2017. Singee77.com