简介
Web Worker 的作用,就是为 JavaScript 创造多线程环境,允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。
Worker 线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker 比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest执行 I/O (尽管responseXML和channel属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码, 通过将消息发布到该代码指定的事件处理程序(反之亦然)。
Web Worker 有以下几个使用注意点。
(1)同源限制
分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源。
(2)DOM 限制
Worker 线程所在的全局对象,与主线程不一样,无法读取主线程所在网页的 DOM 对象,也无法使用document、window、parent这些对象。但是,Worker 线程可以navigator对象和location对象。
(3)通信联系
Worker 线程和主线程不在同一个上下文环境,它们不能直接通信,必须通过消息完成。
(4)脚本限制
Worker 线程不能执行alert()方法和confirm()方法,但可以使用 XMLHttpRequest 对象发出 AJAX 请求。
(5)文件限制
Worker 线程无法读取本地文件,即不能打开本机的文件系统(file://),它所加载的脚本,必须来自网络。
Web Workers API
一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件,这个文件包含将在工作线程中运行的代码。需要注意的是,workers 运行在另一个全局上下文中,不同于当前的window . 因此,在 Worker 内通过 window获取全局作用域将返回错误。
在workers的情况下,DedicatedWorkerGlobalScope 对象代表了worker的上下文(workers是指标准worker仅在单一脚本中被使用。
var myWorker = new Worker('worker.js');
在worker线程中你可以运行任何你喜欢的代码,不过有一些例外情况。比如:在worker内,不能直接操作DOM节点,也不能使用window对象的默认方法和属性。然而你可以使用大量window对象之下的东西,包括WebSockets,IndexedDB等数据存储机制。查看Functions and classes available to workers获取详情。
workers和主线程间的数据传递通过这样的消息机制进行——双方都使用postMessage()方法发送各自的消息,使用onmessage事件处理函数来响应消息(消息被包含在Message事件的data属性中)。这个过程中数据并不是被共享而是被复制。只要运行在同源的父页面中,workers可以依次生成新的workers;并且可以使用XMLHttpRequest 进行网络I/O,但是XMLHttpRequest的responseXML和channel属性总会返回null。
- web worker可以做的事情:
-
- 数据计算;
- 数据请求,XHR,或fetch等(当然也可以Promise);
- navigator(WorkerNavigator),location(WorkerLocation),setTimeout,setInterval等;
-
web worker不可做的事情:
-
- 不能操作DOM节点;(无法访问document对象;)
- 不能使用window默认方法和属性(如alert,history,localStorage等);\
- 主线程和worker线程通信
-
- 使用postMessage,onmessage 来通信;
- 消息被包含在message事件的data属性中;
- 通信的数据不被共享,而是复制;
这个和iframe有点类似,父窗体和iframe窗体之间是相互隔离开的,但是他们都有各自的window上下文,窗体之间通信也是采用postMessage和onmessage,并且还可以跨域。当然,如果不存在跨域情况下,还可以使用dispatchEvent来派发事件进行通信。
在worker中,我们可以拿到一个DedicatedWorkerGlobalScope对象来代表worker的上下文(针对DedicatedWorker的情况)。而在主线程中,我们拿到的则是window对象。
worker中的数据接收和发送
问题:最简便的数据深拷贝的方法是什么?
当然是序列化和反序列化了。
在主页面与 worker 之间传递的数据是通过拷贝,而不是共享来完成的。传递给 worker 的对象需要经过序列化,接下来在另一端还需要反序列化。页面与 worker 不会共享同一个实例,最终的结果就是在每次通信结束时生成了数据的一个副本。
线程安全
Worker接口会生成真正的操作系统级别的线程,如果你不太小心,那么并发会对你的代码产生影响。然而,对于 web worker 来说,与其他线程的通信点会被很小心的控制,这意味着你很难引起并发问题。你没有办法去访问非线程安全的组件或者是 DOM,此外你还需要通过序列化对象来与线程交互特定的数据。所以你要是不费点劲儿,还真搞不出错误来。
Dedicated Worker
一个Dedicated Worker仅仅能被生产它的脚本所使用。下面给出一个小栗子,主线程生成一个worker,在input数据变化的时候,讲数据发送给这个worker,worker内部做一些计算再将计算结果返回给主线程,主线程接收到数据,并将结果显示出来。并且,在这个worker中,还可以创建sub worker,也就是子worker。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Worker</title>
</head>
<body>
<input id="input1" type="text"/>
<h2 id="result"></h2>
<script>
let input1 = document.querySelector("#input1");
let result = document.querySelector("#result");
let myWorker = new Worker('./worker1.js');
myWorker.onmessage = function(e){
result.innerHTML = e.data.value;
}
input1.addEventListener("input", function(){
myWorker.postMessage({
value: this.value
});
});
</script>
</body>
</html>
worker1.js: 在worker内,可以直接使用onmessage,和postMessage。
onmessage = function(e){
console.log(e.data, this.navigator, this.location, this.history, this.localStorage,this.sessionStorage);
postMessage({
value: e.data.value !== '' ? `Hello ${e.data.value}!` : ''
});
// 可以发送xhr,fetch等;
let http = new XMLHttpRequest();
http.onreadystatechange = function(){
if (http.readyState === 4 && http.status === 200) {
// console.log(http.responseText);
console.log('读取数据成功了!');
}
}
http.open('get', './worker1.js', true);
http.send();
}
// worker中可以再创建一个worker
let myWorker2 = new Worker('./worker2.js');
worker2.js
console.log('我是第二个worker');
运行效果:
如何终止worker?
在主线程中立刻终止一个运行中的worker,可以调用terminate方法。worker 线程会被立即杀死,不会有任何机会让它完成自己的操作或清理工作。
myWorker.terminate();
而在worker线程中,worker也可以调用自己的close方法来终止。
close();
如何错误处理?
当 worker 出现运行中错误时,它的 onerror 事件处理函数会被调用。它会收到一个扩展了 ErrorEvent 接口的名为 error的事件。
如何引入脚本库?
Worker 线程能够访问一个全局函数importScripts()来引入脚本,该函数接受0个或者多个URI作为参数来引入资源;以下例子都是合法的:
importScripts(); /* 什么都不引入 */
importScripts('foo.js'); /* 只引入 "foo.js" */
importScripts('foo.js', 'bar.js'); /* 引入两个脚本 */
浏览器加载并运行每一个列出的脚本。每个脚本中的全局对象都能够被 worker 使用。
脚本的下载顺序不固定,但执行时会按照传入 importScripts() 中的文件名顺序进行。这个过程是同步完成的;直到所有脚本都下载并运行完毕,importScripts() 才会返回。
如何生成subWorker?
如果需要的话 worker 能够生成更多的 worker。这就是所谓的subworker,它们必须托管在同源的父页面内。而且,subworker 解析 URI 时会相对于父 worker 的地址而不是自身页面的地址。这使得 worker 更容易记录它们之间的依赖关系。
栗子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web Workers</title>
</head>
<body>
<input id="input1" type="text"/>
<div id="div1">
</div>
<script>
const input1 = document.querySelector("#input1");
const div1 = document.querySelector("#div1");
const worker1 = new Worker("./worker1.js");
worker1.onmessage = function(e){
div1.innerHTML = e.data.value;
}
input1.addEventListener('change', function(e){
worker1.postMessage({
value: e.target.value
});
});
</script>
</body>
</html>
onmessage = function(e){
console.log('worker内接收到了值:', e.data.value);
postMessage({
value: e.data.value + '666'
});
};
let subWorker = new Worker("./sub-worker1.js");
subWorker.onmessage = function(e){
postMessage({
value: e.data.value
});
};
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
if (xhr.readyState === 4) {
console.log("worker请求到了数据:", xhr.responseText);
postMessage({
value: xhr.responseText
});
}
}
xhr.open("GET", "./test.txt", true);
xhr.send();
Shared Worker
一个共享worker可以被多个脚本使用——即使这些脚本正在被不同的window、iframe或者worker访问。
注意: 如果共享worker可以被多个浏览上下文调用,所有这些浏览上下文必须属于同源(相同的协议,主机和端口号)。
DedicatedWorker VS SharedWorker
DedicatedWorker
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dedicated Worker</title>
</head>
<body>
<input id="input1" type="text"/>
<h2 id="result"></h2>
<br/>
<input id="input2" type="text"/>
<h2 id="result2"></h2>
<script>
let myWorker = new Worker('worker1.js');
let input1 = document.querySelector("#input1");
let result = document.querySelector("#result");
input1.addEventListener('change', function(e){
console.log(e.target.value);
myWorker.postMessage({
value: e.target.value
});
});
myWorker.onmessage = function(e){
result.innerText = e.data.value;
}
// 第二个
let myWorker2 = new Worker('worker1.js');
let input2 = document.querySelector("#input2");
let result2 = document.querySelector("#result2");
input2.addEventListener('change', function(e){
myWorker2.postMessage({
value: e.target.value
});
});
myWorker2.onmessage = function(e){
result2.innerText = e.data.value;
}
</script>
</body>
</html>
worker1.js
// DedicatedWorkerGlobalScope
var count = 0;
onmessage = function(e){
console.log("worker: ", e.data.value);
postMessage({
value: e.data.value ? `${e.data.value} - ${++count}` : ''
});
}
运行效果:
DedicatedWorker的数据并不共享,不会相互影响。
SharedWorker
第一个栗子:
index2.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Shared Worker</title>
</head>
<body>
<input id="input1" type="text"/>
<h2 id="result1"></h2>
<br/>
<input id="input2" type="text"/>
<h2 id="result2"></h2>
<script>
// 第一个
let input1 = document.querySelector("#input1");
let result1 = document.querySelector("#result1");
let myWorker1 = new SharedWorker('worker2.js');
input1.addEventListener('change',function(e){
myWorker1.port.postMessage({
value: '第一个:' + e.target.value
});
});
myWorker1.port.onmessage = function(e){
result1.innerText = e.data.value;
}
// 第二个
let input2 = document.querySelector("#input2");
let result2 = document.querySelector("#result2");
let myWorker2 = new SharedWorker('worker2.js');
input2.addEventListener('change',function(e){
myWorker2.port.postMessage({
value: '第二个:' + e.target.value
});
});
myWorker2.port.addEventListener('message', function(e){
result2.innerText = e.data.value;
});
// addEventListener这种写法需要start一下
myWorker2.port.start();
</script>
</body>
</html>
worker2.js
var count = 0;
onconnect = function(e){
let port = e.ports[0];
port.onmessage = function(e){
port.postMessage({
value: `${e.data.value} - ${++count}`
});
}
}
运行效果:
SharedWorker的数据共享,相互影响。
第二个栗子:
index2.html
let input1 = document.querySelector("#input1");
let result1 = document.querySelector("#result1");
let myWorker1 = new SharedWorker('worker2.js');
input1.addEventListener('change',function(e){
myWorker1.port.postMessage({
value: '第一个:' + e.target.value
});
});
myWorker1.port.onmessage = function(e){
result1.innerText = e.data.value;
}
index3.html
let input1 = document.querySelector("#input1");
let result1 = document.querySelector("#result1");
let myWorker1 = new SharedWorker('worker2.js');
input1.addEventListener('change',function(e){
myWorker1.port.postMessage({
value: '第一个:' + e.target.value
});
});
myWorker1.port.onmessage = function(e){
result1.innerText = e.data.value;
}
运行效果:
即使不在同一个window下,也是可以共享的。
官网栗子:github.com/mdn/simple-…
Service Worker
Service workers 本质上充当 Web 应用程序、浏览器与网络(可用时)之间的代理服务器。这个 API 旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。我会在下一篇文章中详细介绍ServiceWorker。