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位为99
work.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.html
const worker = new SharedWorker("/src/channel/sharedWork.js",{name:"main-worker"});
// child.html
const 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 = 10
console.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>