web workers對於每個前端開發者並不陌生,在mdn中的定義:Web Worker為Web內容在后台線程中運行腳本提供了一種簡單的方法。線程可以執行任務而不干擾用戶界面。此外,他們可以使用XMLHttpRequest執行 I/O (盡管responseXML和channel屬性總是為空)。一旦創建, 一個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,不熟悉的話,可以查看相關文檔,這里不多贅述。
腳踏實地行,海闊天空飛~
