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
相关文章
相关标签/搜索