Worker
worker 接口时WebWorker中的一部分,可以创建一个后台运行的脚本,在脚本存活期间可以与调用这建立连接、通信。使用worker只需要调用Worker构造函数
在worker线程内也可以再此创建worker, 嵌套worker必须与创建者同源
在worker中window中部分类无法使用,XHR、fetch对象可以使用,但是XHR responseXML、channel属性始终返回null
worker一旦创建成功就会一直运行,不会被主线程的活动打断。所以一直运行的worker也会比较浪费资源。使用完毕后应该关闭
在使用web worker有几点需要注意:
- 同源策略的限制
分配给worker线程运行的脚本文件,必须与主线程的脚本文件同源
- DOM限制
worker线程所在的全局对象和主线程不一样,无法读取主线程所在网页的DOM对象,比如confirm、document、parent、alert. 但是worker线程中可以使用navigator、location对象,
- 通信的限制
worker线程与主线程不在一个上下文环境,不能直接通信,必须通过特定的方法,比如postMessage
- 文件限制
worker线程无法读取本地的文件,就是不能打开本机的文件系统file://, 但是可以把文件转换以后给到worker线程
基本使用
主线程
const worker = new Worker("xxx/work.js");worker.addEventListener("message", function onMessage(evl) {let data = evl.data; // 由worker线程发出的数据let target = evl.target; // evl.currentTarget, 是发送消息的源,在worker线程里一样});worker.postMessage("array"); // 向worker线程发送一个消息,string// worker.terminate(); // 在主线程中关闭worker线程
woker线程
self.addEventListener("message", function onMessage(evl) {let data = evl.data; // 接受使用线程发来的消息evl.currentTarget.postMessage(`worker : ${data}`); // 增加字符串返回给源线程});// self.close(); 在worker线程中关闭线程
worker API
message
注册一个消息监听器
messageerror
在worker线程接收到一条无法被反序列化的消息时,会触发该事件
error
监听线程之间的错误
postMessage
向线程对中发送一条消息
importScripts
收发数据
在线程之间的通信内容,可以时文本、可以是对象。但是这种是拷贝关系,传值。 在worker线程中修改的内容不会影响到主线程。
主线程和worker线程之间可以交换二进制数据,比如File、Blob、ArrayBuffer等。
// 主线程发送类数组const u8Arr = new Uint8Array(new ArrayBuffer(1));u8Arr[0] = 99;worker.postMessage(u8Arr);worker.addEventListener("message", function onMsg(evl){let data = evl.data; // data【0】为90,和发送之前的u8Arr没有影响});// 在worker线程中收到的data就是u8Arr// worker线程self.addEventListener("message", function onMsg(evl) {let data = evl.data;data[0] = 90; // 在worker线程中的修改对主线程中不会影响evl.target.postMessage(data) // 修改以后返回});
如果一个文件这样拷贝过去很不方便,在主线程内也会阻塞。所以js允许主线程把二进制文件直接转移到子线程,一旦转移主线程就无法继续使用。
// 具体语法worker.postMessage(arrayBuffer, [arrayBuffer])// 主线程const arr = new ArrayBuffer(1);const u8arr = new Uint8Array(arr);u8arr[0]=99; // 在发送前arraybuffer length为1, 0位为99work.postMessage(arr,[arr]);// -> work.postMessage(arr,{transfer:[arr]}); postmessage 的泛型console.log(arr); // 发送后arraybuffer length为0// worker线程self.addEventListener("message", function onMsg(evl) {let data = evl.data;data[0] === 99; // rue});
从scriot创建worker
// worker js标签<script type="worker" >self.addEventListener("message", function onMsg(evl) {let data = evl.data;});</script>// 用于主线程<script >const blob = new Blob([document.querySelector("script[type=worker]").textContent.trim()],{type:"application/javascript"});const workerUrl = URL.createObjectURL(blob);const worker = new Worker(workerUrl);// worker.postMessage('xxx');</script>
SharedWorker
sharedWorker 接口是一种特殊的worker,可以从几个浏览器上下文中访问。具有和其他worker不同的作用域
在chrome中可以在地址栏中输入chrome://inspect/#workers 可以打开service worker和shared worker控制面板,用于调试
使用SharedWorker的几点限制:
- worker资源和使用页面必须是同源,各个页面直接也必须是同源
- 每一个worker连接者之间都是新的port接口
- 如果name不同的情况下可能连接不到
具体使用
main、child两个页面一样(post的消息有加前缀)
//main.html<body><input type="text"id="txt"><button id="send">send</button><br /><br /><label for="result">main:</label><textarea id="result" cols="30" rows="10" readonly></textarea><script>const worker = new SharedWorker("/src/channel/sharedWork.js",{name:"main-worker"});worker.port.start(); // 开启连接const send = document.getElementById('send');const result = document.getElementById("result");const text = document.getElementById("txt");worker.port.addEventListener('message', function (e) {result.value = e.data;}, false);send.addEventListener('click', function (e) {if (text.value === "cancel") {worker.port.close(); //从外部关闭return}worker.port.postMessage(`mian(${text.value})`);// worker.port.postMessage(`child(${text.value})`); // child.htnl}, false);</script></body>
worker.js
const ports = new Set();self.addEventListener("connect", evl => {const port = evl.ports[0];port.start(); // 开启监听port.addEventListener("message", evl => {let data = evl.data;let res = {data,length: ports.size,};// 不可从内部关闭// if (data === "closeWorker") {// port.close();// ports = ports.filter(el => el !== port);// ports.forEach(p => p.postMessage(`close ${evl.name}`))// return// }// 广播发送给所有连接者ports.forEach(p => p.postMessage(JSON.stringify(res, null, 4)));})ports.add(port);})
结果
在mian页面输入
在child页面输入
二者之间输入输出都可以得到响应
如果创建共享worker时,得到的name值不同那两个woker不会出现共享情况,被视为不同的worker:
// main.htmlconst worker = new SharedWorker("/src/channel/sharedWork.js",{name:"main-worker"});// child.htmlconst worker = new SharedWorker("/src/channel/sharedWork.js",{name:"main-worker"});// 如果child.html 的name属性为`child-worker`那俩者之间不会产生联系
关闭对应的worker.port之后,就于worker之间没有了联系,不可于worker之间通信,手动存储的port还会保留,需要手动移除关闭的port
Message Channel
Messag Channel接口可以创建一个异步的新的消息通道,并通过它们两个的端口属性进行收发消息
属于一个类,但很多地方实现了该类的接口,比如worker、iframe, channel可以拿出来单独使用,或者新家一个channel 将所有权给到目标环境
发送的消息值可以是任何有效value,如果发送类数组对象,可以将所有权给出去
使用
<div class="channel-container"><div>child</div><input type="text" id="child-create"><button id="child-send">发送</button><br><textarea name="" id="child-result" cols="30" rows="10" readonly></textarea></div><div class="channel-container"><div>main</div><input type="text" id="main-create"><button id="main-send">发送</button><br><textarea name="" id="main-result" cols="30" rows="10" readonly></textarea></div><script>const channel = new MessageChannel();const main = channel.port1;const child = channel.port2;const mainBox = {send: document.querySelector("#main-send"),create: document.querySelector("#main-create"),result: document.querySelector("#main-result")};const childBox = {send: document.querySelector("#child-send"),create: document.querySelector("#child-create"),result: document.querySelector("#child-result")};function pushEventListener(dom, port) {let tempVal = ``;dom.send.addEventListener("click", function send() {port.postMessage(tempVal);});dom.create.addEventListener("input", function onInput(e) {tempVal = e.target.value;});port.addEventListener("message", function onMessage(e) {dom.result.textContent = e.data;});port.start(); // 手动开启端口, 如果使用onmessage 属性回调的方式会自动开启}pushEventListener(mainBox, main)pushEventListener(childBox, child)</script>
显示的结果:
输入并发送
两个端口之间,postMessage发送消息会触发另一个端口的message事件,比如child发送一次消息,会显示在main的文本域中
交出对象的所有权:
let array = new ArrayBuffer(10);let u8arr = new Uint8Array(array);u8arr[0]=201;u8arr[1]=204;function pushEventListener(dom, port) {let tempVal = ``;dom.send.addEventListener("click", function send() {port.postMessage(array,[array]);});dom.create.addEventListener("input", function onInput(e) {tempVal = e.target.value;});port.addEventListener("message", function onMessage(e) {console.log(e.data); // length = 10console.log(array); // lenght = 0});port.start();}
和worker之间搭配使用:
// 主页面
let worker1 = new Worker('./worker1.js');
let worker2 = new Worker('./worker2.js');
const channel = new MessageChannel();
const main = channel.port1;
const child = channel.port2;
worker2.postMessage("child", [child]) // 先存在监听才可以收到其他端口的消息
worker1.postMessage("main", [main])
// worker2.js
onmessage = function (e) {
const port = e.ports[0];
port.onmessage = portEvent => {
console.log("worker2的port收到:",portEvent.data);
}
console.log(e.data,"work2");
}
// worker1.js
onmessage = function(e) {
console.log("work1收到:",e.data);
e.ports[0].postMessage("worker1发出: "+e.data)
}
/* 结果:
work1收到: main
child work2
worker2的port收到: worker1发出: main
*/
Iframe:
// mian.html
<input type="text" id="child-create">
<button id="child-send">发送</button>
<br>
<textarea name="" id="main-result" cols="30" rows="10" readonly></textarea>
<script>
const childBox = {
send: document.querySelector("#child-send"),
create: document.querySelector("#child-create"),
result: document.querySelector("#main-result")
}
function pushEventListener(dom, port) {
let tempVal = ``;
dom.send.addEventListener("click", function send() {
port.postMessage(tempVal);
});
dom.create.addEventListener("input", function onInput(e) {
tempVal = e.target.value;
});
}
globalThis.onmessage = e => {
childBox.result.textContent = e.data;
}
const iframe = document.createElement("iframe");
iframe.setAttribute("src", "./iframe.html");
iframe.style.setProperty("width", "500px")
iframe.style.setProperty("height", "500px")
iframe.onload = () => {
const port = iframe.contentWindow;
pushEventListener(childBox, port)
}
document.body.insertBefore(iframe, document.body.firstElementChild)
</script>
// iframe.html
<h1>iframe</h1>
<div class="result">
</div>
<script>
console.log(top === window); // 在iframe 中top对象和window不相等
const result = document.querySelector(".result");
globalThis.addEventListener("message", e => {
let data = e.data;
if (data === "send") {
top.postMessage("iframe 发送数据值") // 向顶层对象发送消息
return
}
result.textContent = e.data;
})
</script>
open:
// open
<h1>Open</h1>
<div class="result">
</div>
<script>
const result = document.querySelector(".result");
let globalePort = null;
globalThis.addEventListener("message", e => {
let data = e.data;
let port = e.source;
globalePort = port;
if (data === "send") {
port.postMessage("open 发送数据值"); // 也可以使用opener对象
// opener对象在页面被open打开时会获得源页面的window引用
return
}
result.textContent = e.data;
});
</script>
// mian
<button id="click">跳转</button>
<input type="text" id="child-create">
<button id="child-send">发送</button>
<br>
<textarea name="" id="main-result" cols="30" rows="10" readonly></textarea>
<script>
const childBox = {
send: document.querySelector("#child-send"),
create: document.querySelector("#child-create"),
result: document.querySelector("#main-result")
};
function onMsg(e) {
childBox.result.textContent =e.data;
};
globalThis.name="main";
document.querySelector("#click").addEventListener("click", () => {
port = open("/iframe.html");
port.opener = null;
pushEventListener(childBox);
})
function pushEventListener(dom, port) {
let tempVal = ``;
dom.send.addEventListener("click", function send() {
port.postMessage(tempVal, [ch.port2]);
});
dom.create.addEventListener("input", function onInput(e) {
tempVal = e.target.value;
});
}
globalThis.onmessage = e => {
childBox.result.textContent = e.data;
}
</script>
