web workers对于每个前端开发者并不陌生,在mdn中的定义:Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面。此外,他们可以使用XMLHttpRequest执行 I/O  (尽管responseXMLchannel属性总是为空)。一旦创建, 一个worker 可以将消息发送到创建它的JavaScript代码, 通过将消息发布到该代码指定的事件处理程序(反之亦然)。

我的理解:web workers可以为js带来多线程环境,由js主线程创建并独立于js主线程处理一些任务,同时也不会阻塞js主线程代码的执行。

在我们日常开发中,主要使用这三类worker:DedicatedWorker,ServiceWorker,SharedWorker。其中,DedicatedWorker主要是在浏览器中单开一个私有线程,可以缓解js单线程中对一些复杂业务逻辑的处理压力。ServiceWorker主要实现页面资源的缓存,也是PWA应用中的重要组成部分,提升用户离线体验。SharedWorker常用于跨标签页通讯(必须是同源的浏览器上下文),共享信息,本文将使用SharedWorker实现多标签页联动的计时器的demo,完整代码也将全部贴出。

demo效果:

由于web worker有同源限制,需要启用本地服务。

1.npm init -y

2.npm i vite -S

package.json:

{
"name": "sharedworker_timer",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "vite"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"vite": "^2.7.1"
}
}
npm run dev

项目目录结构:

页面视图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>timer by sharedWorker</title>
</head> <body>
<div class=timer>
<h1 class="timerNum">00:00:00</h1>
<div class="timerButtonGroup">
<button class="startBtn">开始</button>
<button class="pauseBtn">暂停</button>
<button class="resetBtn">重置</button>
</div>
</div>
<script type="module" src="./src/app.js"></script>
</body> </html>

app.js:

import './CreateWorker'; // 引入sharedworker,注册message事件

; (function (doc) {

    let worker = new SharedWorker('./src/worker.js').port; // 使用sharedworker发送message事件

    const oStartBtn = doc.querySelector('.startBtn');
const oPauseBtn = doc.querySelector('.pauseBtn');
const oResetBtn = doc.querySelector('.resetBtn'); const init = () => { // 初始化绑定事件
bindEvent();
} function bindEvent() {
oStartBtn.addEventListener('click', handdleStartBtnClick, false); // 开始按钮点击事件
oPauseBtn.addEventListener('click', handdlePauseBtnClick, false); // 暂停按钮点击事件
oResetBtn.addEventListener('click', handdleResetBtnClick, false); // 重置按钮点击事件
} function handdleStartBtnClick() {
worker.postMessage({ // 给worker发送消息
data: {
event: 'start',
}
})
} function handdlePauseBtnClick() {
worker.postMessage({
data: {
event: 'pause',
}
})
} function handdleResetBtnClick() {
worker.postMessage({
data: {
event: 'reset',
}
})
} init(); })(document)

worker.js:

let ports = new Map(),
connectList = [],
textlist = [],
nolist = [];
self.addEventListener('connect', function (e) {
var port = e.ports[0]
port.start();
port.addEventListener('message', function (e) {
var worker = e.currentTarget,
res = e.data;
if (connectList.indexOf(worker) === -1) { // 收集所有连接的标签页
connectList.push(worker)
} switch (res.status) {
case 0: // 标签页第一次连接
inform(function (item) {
if (item != worker) { // 如果当前连接的标签页不是自己,发送该信息
item.postMessage('有新页面加入 历史标签页总数' + connectList.length);
} else { // 当前标签页是自己,打印自己标签号码
item.postMessage('我是新页面编号' + connectList.length);
}
});
break;
case 1:
nolist.push(res.no);
inform(function (item) {
item == worker && item.postMessage('自己的号码牌为' + res.no + JSON.stringify(nolist));
});
break;
case 'heartbeat': // 心跳
ports.set(res, +new Date())
break;
default:
textlist.push(res.data);
inform(textlist);
break;
}
})
setInterval(() => {
let now = +new Date()
for (var [key, value] of ports) {
console.log(now - value + " = " + value);
if (now - value > 3100) {
// 3秒以上没发心跳,标签页已关闭,关闭相应的worker线程
ports.delete(key) }
}
}, 1000)
}); // 分发消息
function inform(obj) {
var cb = (typeof obj === 'function') ? obj : function (item) {
item.postMessage(obj);
}
connectList.forEach(cb);
}

CreateWorker.js

import TimerEventList from './timer'; // 引入计时器类

; (function (doc) {
const timerDom = doc.querySelector('.timerNum');
const timerEventList = TimerEventList.create(timerDom); // 创建计时器 let no;
let worker = new SharedWorker('./src/worker.js').port;
worker.start()
worker.addEventListener('message', (res) => {
// console.log('来自worker的数据:', res.data)
if (res.data && res.data.includes('我是新页面编号')) {
no = res.data.replace('我是新页面编号', '')
worker.postMessage({
status: 1,
no,
});
return
}
let e = res.data[res.data.length - 1]; if (e.event == "start") { // 如果worker中event为start
timerEventList.notify('start') // 发布start事件
}
if (e.event == "pause") {
timerEventList.notify('pause')
}
if (e.event == "reset") {
timerEventList.notify('reset')
}
}, false)
worker.postMessage({ // 第一次加入worker
status: 0,
});
function heartbeat() {
worker.postMessage({
'status': 'heartbeat',
'data': no
})
}
heartbeat()
setInterval(() => { // 心跳检测
heartbeat()
}, 3000);
})(document)

下面是计时器模块(./src/timer):

index.js

import ChangeDom from "./ChangeDom"; // 引入改变dom类
import ClickEvent from "./ClickEvent"; // 引入点击事件类 const EVENT_TYPE = { // 事件类型
start: 'start',
pause: 'pause',
reset: 'reset'
} class TimerEventList { static instance ;
TimerDom;
clickEvent;
changeDom;
startHandlers = [];
pauseHandlers = [];
resetHandlers = []; constructor(TimerDom) {
this.TimerDom = TimerDom;
this.initTimer();
}
  // 单例模式
static create(timerDom) {
if (!TimerEventList.instance) {
TimerEventList.instance = new TimerEventList(timerDom);
}
return TimerEventList.instance;
} initTimer() {
this.clickEvent = ClickEvent.create();
this.changeDom = ChangeDom.create(this.TimerDom); for (let k in EVENT_TYPE) {
this.initHandlers(EVENT_TYPE[k]);
} } initHandlers(type) { // 处理对应点击事件,并将点击的事件和对应操作dom推入对应的handlers数组
switch (type) {
case EVENT_TYPE.start:
this.startHandlers.push(this.clickEvent.startClick.bind(this.clickEvent));
this.startHandlers.push(this.changeDom.startTimer.bind(this.changeDom));
break;
case EVENT_TYPE.pause:
this.pauseHandlers.push(this.clickEvent.pauseClick.bind(this.clickEvent));
this.pauseHandlers.push(this.changeDom.pauseTimer.bind(this.changeDom));
break;
case EVENT_TYPE.reset:
this.resetHandlers.push(this.clickEvent.resetClick.bind(this.clickEvent));
this.resetHandlers.push(this.changeDom.resetTimer.bind(this.changeDom));
break;
default:
break;
}
}
  // 观察者模式
notify(type) { // 订阅计时器相关点击和dom操作事件
let i=0,
handlers = [],
res;
switch (type) {
case EVENT_TYPE.start:
handlers = this.startHandlers;
break;
case EVENT_TYPE.pause:
handlers = this.pauseHandlers;
break;
case EVENT_TYPE.reset:
handlers = this.resetHandlers;
break;
default:
break;
}
res = handlers[i](); while (i < handlers.length - 1) {
i++;
res = res.then(param => {
return handlers[i](param);
})
}
}
} export default TimerEventList;

ClickEvent.js

class ClickEvent {

    static instance;
  // 单例
static create() {
if (!ClickEvent.instance) {
ClickEvent.instance = new ClickEvent();
}
return ClickEvent.instance;
} startClick() { // 返回Promise,方便做一些异步操作,本demo中没有涉及相关异步操作,这里留作扩展
return new Promise((resolve, reject) => {
resolve('start')
})
} pauseClick() {
return new Promise((resolve, reject) => {
resolve('pause')
})
} resetClick() {
return new Promise((resolve, reject) => {
resolve('reset')
})
}
} export default ClickEvent;

ChangeDom.js

import Time from './Time';

class ChangeEvent {

    static instance;
time;
timerDom; constructor(timerDom) { // 构造器接收dom对象,这里dom对象为页面时间的显示节点
this.timerDom = timerDom;
this.time = Time.create(this.timerView.bind(this)) // 将操作dom的方法传入Time类中,作为回调使用,这里bind改变this指向
}
  // 单例
static create(timerDom) {
if (!ChangeEvent.instance) {
ChangeEvent.instance = new ChangeEvent(timerDom);
}
return ChangeEvent.instance;
} startTimer() {
console.log('开始计时!');
this.time.timeStart();
} pauseTimer() {
console.log('暂停计时!');
this.time.timePause();
} resetTimer() {
console.log('结束计时!');
this.time.timeReset();
this.timerDom.innerText = '00:00:00';
} timerView(v) {
console.log('=====>', v)
this.timerDom.innerText = v;
}
} export default ChangeEvent;

Time.js

class Time {

    static instance;
hour = 0;
minute = 0;
second = 0;
str = '00:00:00';
timer = null;
cb = null; constructor (cb) {
this.cb = cb;
}
  // 单例
static create(cb) { // 接收回调函数,用于后面更新dom
if (!Time.instance) {
Time.instance = new Time(cb);
}
return Time.instance;
} timeStart() {
clearInterval(this.timer) // 开启新的计时器之前,先清除一遍计时器,避免双重计时
this.timer = setInterval(this.timeCallback.bind(this), 1000)
} timePause() {
this.timer&&clearInterval(this.timer);
} timeReset() {
this.timer&&clearInterval(this.timer);
this.hour = 0;
this.minute = 0;
this.second = 0;
this.str = '00:00:00';
} timeCallback() { // 计时器主要方法,用于改变小时、分钟、秒,返回新的时间字符串,以及更新dom
this.second = this.second + 1
if (this.second >= 60) {
this.second = 0
this.minute = this.minute + 1
}
if (this.minute >= 60) {
this.minute = 0
this.hour = this.hour + 1
}
this.str = this.toDub(this.hour) + ':' + this.toDub(this.minute) + ':' + this.toDub(this.second);
this.cb(this.str);
} toDub(n) { // 补0
if (n < 10 ) {
return '0' + n
}else {
return '' + n
}
}
} export default Time;

以上就是使用SharedWorker实现计时器的完整过程和代码,基本实现了多标签页之间计时器同步开始计时、暂停、重置等功能,掌握本demo,相信你也可以用SharedWorker解决绝大多数跨标签页通信的场景!如果有什么问题,欢迎留言讨论~

注:本文用到的SharedWorker相关api,不熟悉的话,可以查看相关文档,这里不多赘述。

脚踏实地行,海阔天空飞~

SharedWorker实现多标签页联动计时器的更多相关文章

  1. 实现多个标签页之间通信的几种方法(sharedworker)

      效果图.gif prologue 之前在网上看到一个面试题:如何实现浏览器中多个标签页之间的通信.我目前想到的方法有三种:使用websocket协议.通过localstorage.以及使用html ...

  2. 最新 去掉 Chrome 新标签页的8个缩略图

    chrome的新标签页的8个缩略图实在让人不爽,网上找了一些去掉这个略缩图的方法,其中很多已经失效.不过其中一个插件虽然按照原来的方法已经不能用了,但是稍微变通一下仍然是可以用的(本方法于2017.1 ...

  3. 在QMainWindow中利用多个QDockWidget构成标签页tab(原创)

    功能描述: 在QMainWindow下,使用多个QDockWidget构成可切换,可拖动,可关闭的标签页:标签页的切换由相关联的QAction触发. 实现效果: 代码如下: QDockWidget * ...

  4. vim 标签页 tabnew 等的操作命令

    对于vim这个 ide来说, 单纯的用 多子窗口 来操作, 感觉还是不够的, 还要结合标签页tab pages来,才能更好的操作. 所有关于标签 的 命令行 命令都是 以 :tab开始的, 可以用ta ...

  5. Web编程基础--HTML、CSS、JavaScript 学习之课程作业“仿360极速浏览器新标签页”

    Web编程基础--HTML.CSS.JavaScript 学习之课程作业"仿360极速浏览器新标签页" 背景: 作为一个中专网站建设出身,之前总是做静态的HTML+CSS+DIV没 ...

  6. EasyUI创建异步树形菜单和动态添加标签页tab

    创建异步树形菜单 创建树形菜单的ul标签 <ul class="easyui-tree" id="treeMenu"> </ul> 写j ...

  7. Tabio – 轻松,高效的管理 Chrome 标签页

    Tabio 是一个 Chrome 扩展,旨在简化大量浏览器标签页的管理.它提供的搜索功能允许您快速.轻松地找到您需要的选项卡.Tabio 便于组织你的标签,简单的拖拽排序.您也可以使用输入.删除和箭头 ...

  8. MFC MDI 主框架和标签页数据互操作

    ==================================声明================================== 本文原创,转载在正文中显要的注明作者和出处,并保证文章的完 ...

  9. jquery插件之tab标签页或滑动门

    该插件乃本博客作者所写,目的在于提升作者的js能力,也给一些js菜鸟在使用插件时提供一些便利,老鸟就悠然地飞过吧. 此插件旨在实现目前较为流行的tab标签页或滑动门特效,在此插件中默认使用的是鼠标滑过 ...

随机推荐

  1. 菜鸡的Java笔记 - java 常用类库

    CommonClassLibrary 常用类库        定时调度            定时调度指的是每到一个时刻,都会自动的产生某些特定的操作形式                    con ...

  2. vue + cesium开发(4) 绘制图形

    在官方例子中每个图形都是一个entity,官方例子提供了显示正方形.圆形.锥形.图片等多种案例! // 初始花 var viewer = new Cesium.Viewer("cesiumC ...

  3. python实现圆检测

    目录: (一)霍夫圆检测原理 (二)代码实现 (一)霍夫圆检测原理 (二)代码实现 1 #霍夫圆检测 2 import cv2 as cv 3 import numpy as np 4 5 def d ...

  4. <C#任务导引教程>练习三

    /*Convert.ToInt("213165");int a=12345;string sn=a.ToString();//把a转换成字符串snint b=int.Parse(s ...

  5. OpenShift 本地开发环境配置(基于 Minishift)

    本文要做什么? 很多为了验证应用在 OpenShift 平台的行为是否正常,或者组成一个简单的开发环境,直接搭建一个 OpenShift/Origin 环境可能太重了,而且运行在本机可能占用内存也太多 ...

  6. [cf1285F]Classical

    先枚举$d=\gcd$,然后暴力枚举所有$d$的倍数,相当于求出若干个数中最大的互素对 假设选出的数依从大到小排序后为$a_{i}$,令$g_{i}=\min_{(a_{i},a_{j})=1}j$, ...

  7. idea插件 Background Image Plus 随机更换背景图片

    首先在市场搜索: Background Image Plus 设置图片: 在view中,有set 图片,有random图片,有clean图片的 设置就是用set,随便设置个路径. 重点来了,随机更换背 ...

  8. 学习 DDD 之消化知识!

    接触到DDD到现在已经有8个月份了,目前所维护的项目也是基于DDD的思想开发的,从一开始的无从下手,到现在游刃有余,学到不少东西,但是都是一些关键字和零散的知识,同时我也感受到了是因为我对项目越来越熟 ...

  9. OI省选算法汇总及学习计划(转)

    1.1 基本数据结构 数组(√) 链表(√),双向链表(√) 队列(√),单调队列(√),双端队列(√) 栈(√),单调栈(√) 1.2 中级数据结构 堆(√) 并查集与带权并查集(√) hash 表 ...

  10. [ARC101C] Ribbons on Tree

    神仙的容斥题与神仙的树形DP题. 首先搞一个指数级的做法:求总的.能够覆盖每一条边的方案数,通过容斥可以得到\(\text{ans}=\sum\limits_E{(-1)^{|E|}F(E)}\).其 ...