Web Workers
- 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作为参数来引入资源
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 () {
// 它的URL指定了数据模式或者blob
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
});
};
})();
// 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})()");

更多精彩