Web Workers 概念与用法

http://www.w3school.com.cn/html5/html_5_webworkers.asp



      Web Workers 是一种不是在页面线程而是在后台线程中执行脚本的技术。Web Workers 的优点在于可以将那些复杂的计算过程放在一个单独的后台线程中执行,进而使得主线程(通常是UI线程)能够不被阻塞,也不会被减缓。

    

      所谓的 worker 就是用一个构造器创建的 (例如 Worker()) ,用来运行一个 JavaScript 文件的函数 — 这个文件包含了将要在 worker 线程中执行的代码,并且 worker 将在一个与当前 window 对象不同的全局上下文环境中运行。这个上下文环境由一个叫做 DedicatedWorkerGlobalScope 的对象来表示,并且会根据检测到的 worker 的类型有所不同 (这里说明的是标准的基于单个脚本的 worker,对于公用的 worker,则是 SharedWorkerGlobalScope)。

你可以在你的 worker 线程中运行任意的代码,以下情况除外:你不能直接在 worker 线程中操纵 DOM 元素, 或者使用某些 window 对象中默认的方法和属性。 但是 window 对象中很多的方法和属性你是可以使用的,包括 WebSockets,以及诸如 IndexedDB 和 FireFox OS 中独有的 Data Store API 这一类数据存储机制。更多信息请参见: Functions and classes available to workers 。

主线程和 worker 线程之间通过这样的方式互相传输信息:两端都使用 postMessage() 方法来发送信息, 并且通过 onmessage 这个 event handler来接收信息。 (传递的信息包含在Message 这个事件的数据属性内) 。数据的交互是通过传递副本,而不是直接共享数据。

一个 worker 可以生成另外的新的 worker,这些 worker 的宿主和它们父页面的宿主相同。  此外,worker 可以通过 XMLHttpRequest 来访问网络,只是XMLHttpRequest 的 responseXML 和 channel 这两个属性将总是 null 。

除了专用 worker 之外,还有一些其他种类的 worker :

  • Shared workers are workers that can be utilized by multiple scripts running in different windows, IFrames, etc., as long as they are in the same domain as the worker. They are a little more complex than dedicated workers — scripts must communicate via an active port. See SharedWorker for more details.
  • ServiceWorkers essentially act as proxy servers that sit between web applications, and the browser and network (when available). They are intended to (amongst other things) enable the creation of effective offline experiences, intercepting network requests and taking appropriate action based on whether the network is available and updated assets reside on the server. They will also allow access to push notifications and background sync APIs.
  • Chrome Workers are a Firefox-only type of worker that you can use if you are developing add-ons and want to use workers in extensions and have access tojs-ctypes in your worker. See ChromeWorker for more details. 
  • Audio Workers provide the ability for direct scripted audio processing to be done inside a web worker context.
      

       

       专用 Web Worker (Dedicated Web Worker) 提供了一个简单的方法使得 web 内容能够在后台运行脚本。一旦 worker 创建后,它可以向由它的创建者指定的事件监听函数传递消息,这样该 worker 生成的所有任务就都会接收到这些消息。worker 线程能够在不干扰 UI 的情况下执行任务。另外,它还能够使用 XMLHttpRequest (虽然 responseXML 与 channel 两个属性值始终是 null)来执行  I/O 操作。查看 worker 的参考文档Worker;本文通过提供例子和细节补全了前面的文档。提供给 worker 的函数 列出了 worker 所支持的函数。

关于线程安全Edit

    Worker 接口会生成真正的操作系统级别的线程,如果你不太小心,那么并发(concurrency)会对你的代码产生有趣的影响。然而,对于 web worker 来说,与其他线程的通信点会被很小心的控制,这意味着你很难引起并发问题。你没有办法去访问非线程安全的组件或者是 DOM,此外你还需要通过序列化对象来与线程交互特定的数据。所以你要是不费点劲儿,还真搞不出错误来。

生成 workerEdit

       创建一个新的 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 来说是一个有效的脚本。

传递数据Edit

       在主页面与 worker 之间传递的数据是通过拷贝,而不是共享来完成的。传递给 worker 的对象需要经过序列化,接下来在另一端还需要反序列化。页面与 worker 不会共享同一个实例,最终的结果就是在每次通信结束时生成了数据的一个副本。大部分浏览器使用结构化拷贝来实现该特性。

在往下进行之前,出于教学的目的,让我们创建一个名为 emulateMessage() 的函数,它将模拟在从 worker 到主页面(反之亦然)的通信过程中,变量的拷贝而非共享行为:

function emulateMessage (vVal) {
    return eval("(" + JSON.stringify(vVal) + ")");
}

// Tests

// test #1
var example1 = new Number(3);
alert(typeof example1); // object
alert(typeof emulateMessage(example1)); // number

// test #2
var example2 = true;
alert(typeof example2); // boolean
alert(typeof emulateMessage(example2)); // boolean

// test #3
var example3 = new String("Hello World");
alert(typeof example3); // object
alert(typeof emulateMessage(example3)); // string

// test #4
var example4 = {
    "name": "John Smith",
    "age": 43
};
alert(typeof example4); // object
alert(typeof emulateMessage(example4)); // object

// test #5
function Animal (sType, nAge) {
    this.type = sType;
    this.age = nAge;
}
var example5 = new Animal("Cat", 3);
alert(example5.constructor); // Animal
alert(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 workertitle>
<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 secondsa>li>
    <li><a href="javascript:oMyTask.terminate();">terminate() the Workera>li>
  ul>
body>
html>

my_task.js (the worker):

// your custom PRIVATE functions

function 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 functions

function 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); // 32MB
for (var i = 0; i < uInt8Array .length; ++i) {
  uInt8Array[i] = i;
}

worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

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

生成subworkerEdit

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

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

嵌入式 workerEdit

目前没有一种「官方」的方法能够像 

你可能感兴趣的:(Web Workers 概念与用法)