SharedWorker
代表一种特定类型的 Worker
,可以从多个浏览器上下文中访问,例如多个窗口、iframe 或其它 worker。它实现了一个不同于普通 Worker
的接口,具有不同的全局作用域 SharedWorkerGlobalScope
(en-US)
注意:如果要使 SharedWorker
连接到多个不同的页面,这些页面必须是同源的(相同的协议、host 以及 端口)
一、与普通 Worker
的不同
-
普通
Worker
每次new
都会创建一个新的 worker线程,而SharedWorker
对于相同url
脚本只会创建一个共享线程。每个与SharedWorker
连接的主线程,在SharedWorker
中都会有一个port
对象(稍后讲解)与之对应 -
SharedWorker
通过.port
来发送、接收消息
二、SharedWorker
具有的属性
2.1 .onerror
一个 EventListner
,当 error
类型的事件冒泡到 worker
时触发
2.2 .port
一个 MessagePort
对象,该对象用来与 shared worker
通信、控制
三、MessagePort
对象
在 主线程 和 shared worker线程 中,双方都是通过 MessagePort
对象进行通信的
主线程中:通过 new SharedWorker()
返回对象的 .port
属性即可拿到 MessagePort
对象
worker线程中:通过 onconnect
事件对象 event.ports[0]
拿到对应的 MessagePort
对象
3.1 方法
3.1.1 MessagePort.postMessage
从端口发送一条消息,并且可选是否将对象的所有权交给其他浏览器上下文
对象所有权:
worker线程与主线程之间的数据通信是值传递,需要先串行化成字符串,再还原
如果传500M的二进制对象,生成原文件的拷贝显然不现实
所以可以让主线程直接将对象所有权交给对方,这样worker线程就快速有了对该二进制数据的操作权;同时主线程也会失去对该数据的操作权,防止出现同时修改数据的局面
3.1.2 MessagePort.start
启动发送该端口的消息队列;在未调用之前,发往该端口的消息都在队列中等待
简而言之就是:
主线程不调用
port.start()
,就没法接受到worker线程发来的消息;但还是可以向worker线程发送消息的
只有用 port.addEventListenr('message')
时需要手动显式地调用该方法
若使用 port.onmessage
会默认隐式调用 port.start()
主线程、worker线程中都需要调用:
worker.port.addEventListener("message", (event) => {
console.log(`接收到了消息:${event.data}`);
});
worker.port.start();
# 等同于
worker.port.onmessage = (event) => {
console.log(`接收到了消息:${event.data}`);
};
3.1.3 MessagePort.close
断开端口连接,它将不再是激活状态
3.2 事件回调
3.2.1 MessagePort.onmessage
收到消息时触发
3.2.2 MessagePort.onmessageerror
端口收到的消息无法被反序列化
四、示例
4.1 基本使用
该基本示例来自 MDN
在这个 shared worker 例子中 (运行 shared worker), 我们有两个 HTML 页面。每个页面都用了同一个 shared worker
执行计算,即使在不同窗口内运行
下面的代码展示了如何通过 SharedWorker()
方法来创建一个共享进程对象:
var myWorker = new SharedWorker("worker.js");
这会触发 worker 线程的 connect
事件,worker 线程在connect
事件中启动与主线程关联的端口后,即可接受主线程发来的消息
主线程通过 MessagePort
对象与 worker 通信,这个对象由SharedWorker.port
(en-US) 属性获得:
/* 主线程 向 shared worker 发送消息 */
first.onchange = function() {
myWorker.port.postMessage([first.value,second.value]);
console.log('Message posted to worker');
}
second.onchange = function() {
myWorker.port.postMessage([first.value,second.value]);
console.log('Message posted to worker');
}
/* 主线程 接收来自 shared worker 的消息 */
myWorker.port.onmessage = function(e) {
result1.textContent = e.data;
console.log('Message received from worker');
}
在 worker 中我们需要使用 SharedWorkerGlobalScope.onconnect
(en-US) 处理 主线程 连接到 worker线程 的事件
可以在 connect
事件 event.ports
属性中获取到 主线程 与 worker 关联的端口对象(每个主线程对应一个新的 port
)
然后我们使用 port.start()
方法开始接收该端口的消息队列,并通过监听 onmessage
事件处理来自主线程的消息:
onconnect = function(e) {
var port = e.ports[0];
port.addEventListener('message', function(e) {
var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
port.postMessage(workerResult);
});
/* 开始发送该端口中的消息队列,只有用`port.addEventListenr('message')`时需要手动显式调用 */
/* 使用`port.onmessage`会默认隐式调用`port.start()` */
port.start();
}
下个例子是更详细的运用:
4.2 向各个页面广播
在基本示例中,2个页面只是单独与work线程通信。本例中,我们将实现 worker线程 向所有连接了的主线程广播消息
原理:worker 中存下与每个主线程关联的 port
。广播消息时通过遍历,以向所有主线程发送数据。由于自己维护了一个 portPool
数组,断开连接时,也需要通知 worker 删除数组中对应的 port
sharedWorker.js:
/* 存下与 shared worker 连接了的所有端口 */
const portPool = [];
/* 连接事件触发 */
self.onconnect = function (e) {
console.log("worker内:连接事件触发");
const port = e.ports[0];
// 将 port 添加到 portPool 中
portPool.push(port);
port.onmessage = (e) => {
console.log(`worker 接收到了消息:${e.data}`);
if (e.data === "Close") {
// 关闭连接,移除对应 port
const index = portPool.findIndex((p) => p === port);
portPool.splice(index, 1);
port.close();
// 广播,连接数量
boardcast(portPool.length);
}
};
// 广播,连接数量
boardcast(portPool.length);
};
/* 向当前所有连接了的 port 广播消息 */
function boardcast(message) {
portPool.forEach((port) => {
port.postMessage(message);
});
}
这样我们就实现了向多个页面广播消息的功能
在页面中使用:
const SharedWorkerPage: FC<IProps> = memo(({}) => {
const workerRef = useRef<SharedWorker | null>(null);
const [connectCount, setConnectCount] = useState<number>(0);
/* 通知断开与 shared worker 的连接 */
const closeSharedWorkerPort = useCallback(() => {
if (workerRef.current) {
workerRef.current.port.postMessage("Close");
workerRef.current = null;
}
}, []);
/* 初始化 */
useEffect(() => {
const worker = new SharedWorker(
`${window.location.origin}${window.location.pathname}sharedWorker/sharedWorker.js`
);
workerRef.current = worker;
worker.port.postMessage("Hello");
worker.port.onmessage = (event) => {
console.log(`主线程 接收到了 worker 消息:${event.data}`);
// 接受一个当前有多少个页面连接了 shared worker 的个数
const connectCount = event.data;
setConnectCount(connectCount);
};
/**
* 正常情况下,页面关闭,即使不手动调`port.close()`,`shared worker`也会自动关闭与该页面的连接
* 当没有页面与`shared worker`连接时,就`worker`自动删除了
*
* 但由于这里我们自己在`worker`中维护了 portPool 数组,来记录当前有多少个连接
*
* 若直接改地址栏url,导致组件没有走卸载生命周期
* 此时`shared worker`没有收到"Close"消息,就不会将与该页面对应的port移除portPool数组,导致记录的连接数出错
* 故再监听window "beforeunload",以发送"Close"消息
*/
window.addEventListener("beforeunload", closeSharedWorkerPort);
return () => {
/* 组件卸载时,通知关闭与 shared worker 的连接 */
closeSharedWorkerPort();
window.removeEventListener("beforeunload", closeSharedWorkerPort);
};
}, []);
return (
<div className={PREFIX}>
当前有 {connectCount} 页面连接到了 shared worker
</div>
);
});
七、调试 shared worker
shared worker
中 console.log
输出的信息,在页面的控制台中无法看到
对于chrome,在浏览器地址栏输入 chrome://inspect
点击 Shared workers
> inspect
即可看到 shared worker
的 console.log
输出: