Web Worker 介紹
眾所周知,JavaScript 這門語言的一大特點就是單執行緒,即同一時間只能同步處理一件事情,這也是這門語言衍生出的 nodeJS 被各后端大佬詬病的很重要的一點,
然而,JavaScript 在設計之初,其實是故意被設計成單執行緒語言的,這是由于它當時的主要用途決定的,
JavaScript 最初的設計初衷是完成頁面與用戶的互動,操作 DOM 或者 BOM 元素,此時如果一味地追求效率使用多執行緒的話,會帶來資源搶占,資料同步等等問題,因此必須規定,同一時間只有一個執行緒能直接操作頁面元素,以保證系統的穩定性以及安全性,
盡管如此,但是 JavaScript 并不是只能線性處理任務,JS 擁有訊息佇列和事件回圈機制,通過異步處理訊息的能力來實作并發,在高 I/O 型并發事務處理的程序中,由于不需要手動生成與銷毀執行緒以及占用額外管理執行緒的空間,性能表現及為優異,因此,nodeJS 作為 JavaScript 在服務端的探索者,在處理高并發網路請求的優勢極為明顯,
盡管 JavaScript 通過異步機制完美解決了高 I/O 性能的問題,但 JavaScript 單執行緒執行的本質還是沒有變的,因此缺點就顯而易見了,那就是處理 CPU 密集型的事務時沒有辦法充分調動現代多核心多執行緒機器的運算資源,
在現代大型前端專案中,隨著代碼的復雜程度越來越高,本地的計算型事務也在變得繁重,而運行在單執行緒下 JS 專案必定會忙于處理計算而無暇顧及用戶接下來的頻繁操作,造成卡頓等不太好的用戶體驗,更嚴重的情況是,當計算型事務過多時還有可能因為資源被占滿帶來網頁無回應的卡死現象,因此,Web 專案的本地多執行緒運算能力勢在必行,由此,Web Worker 應運而生了,
Web Worker 是 HTML5 中推出的標準,官方是這樣定義它的:
Web Workers makes it possible to run a script operation in a background thread separate from the main execution thread of a web application.
它允許 JavaScript 腳本創建多個執行緒,從而充分利用 CPU 的多核計算能力,不會阻塞主執行緒 (一般指 UI 渲染執行緒) 的運行,
Web Worker 雖然是 HTML5 標準,但其實早在 2009 年 W3C 就已經提出了草案,因此它的兼容性良好,基本覆寫了所有主流瀏覽器,
Web Worker 的局限
需要注意的是,Web Worker 本質上并沒有突破 JavaScript 的單執行緒的性質,
事實上,Web Worker 腳本中的代碼并不能直接操作 DOM 節點,并且不能使用絕大多數 BOM API,它的全域環境是 DedicatedWorkerGlobalScope 而并不是 Window,運行 Worker 的實際上是一個沙箱,跑的是與主執行緒完全獨立 JavaScript 檔案,
Worker 做的這些限制,實際上也是為了避免文章開頭說過的搶占問題,它更多的使用場景是作為主執行緒的附屬,完成高 CPU 計算型的資料處理,再通過執行緒間通信將執行結果傳回給主執行緒,在整個程序中,主執行緒仍然能正常地相應用戶操作,從而很好地避免頁面的卡頓現象,
Web Worker 的使用
新建
目前 Web Worker 的瀏覽器支持已經較為完善,基本上直接傳入 Worker 腳本的 URI 并實體化即可使用,
/* main.js */
const worker = new Worker("./worker.js")
通信
Worker 與主執行緒之間的通信只需要各有兩個 API:onmessage/addEventListener 與 postMessage 即可完成收發訊息的互動,
/* main.js */
const worker = new Worker("./worker.js");
// 主執行緒發送訊息
worker.postMessage({ data: 'mainthread send data' });
// 主執行緒接收訊息
worker.onmessage = (e) => {
const { data } = e;
if (!data) return;
console.log(data);
}
/* worker.js */
// worker執行緒接收訊息
self.addEventListener('message', (e) => {
const { data } = e;
if (!data) return;
// worker執行緒發送訊息
self.postMessage({data: 'worker received data'})
});
注:Worker 中,this.xx, self.xx 與直接使用 xx,其作用域都指向 worker 的全域變數 DedicatedWorkerGlobalScope ,可以互換,
銷毀
Worker 的銷毀方式有兩種,既能在內部主動銷毀,也能夠被主執行緒通知銷毀,
/* main.js */
worker.terminate();
/* worker.js */
self.close();
進階:讓通信方式 Promise 化
根據上一節,我們已經能夠簡單地使用 Worker 的 API 來獲取瀏覽器多執行緒計算的能力,但是它離工程化的應用還缺少了一些易用性,比如我們多數時候需要使用到的異步相應,接下來我們就來做這件事情,
首先我們需要一個異步回呼集合 actionHandlerMap,用于存放等待 Worker 回應的 Promise resolve 方法,其 key 值可以用通信中的某一 id 指定(保證其唯一性即可),接著我們需要封裝一下原生的 postMessage 與 onmessage 方法,
我們在原生的 postMessage 發送的資訊中加入 id,并將當前的 Promise 的 resolve 方法放入 actionHandlerMap,等待 Worker 回傳結果后觸發,
對于 onmessage 的監聽,在接收到 Worker 發送過來的回應之后,匹配回應的 Promise 并執行 .then() 方法,完成后洗掉集合中的 Promise resolve 函式,
/* main.js */
let fakeId = 0;
class MainThreadController {
constructor(options) {
this.worker = new Worker(options.workerUrl, { name: options.workerName });
// 等待異步回呼集合
this.actionHandlerMap = {};
this.worker.onmessage = this.onmessage.bind(this);
}
onmessage(e) {
const { id, response } = e.data;
if(!this.actionHandlerMap[id]) return;
// 執行相應的 Promise resolve
this.actionHandlerMap[id].call(this, response);
delete this.actionHandlerMap[id];
}
postMessage(action) {
// 實際使用中,可以指定或生成一個業務 id 作為 key 值
const id = fakeId++;
return new Promise((resolve, reject) => {
const message = {
id,
...action,
};
this.worker.postMessage(message);
this.actionHandlerMap[id] = (response) => {
resolve(response);
};
});
}
}
const mainThreadController = new MainThreadController({ workerUrl: './worker.js', workerName: 'test-worker' });
mainThreadController
.postMessage({
actionType: 'asyncCalc',
payload: { msg: 'send messages to worker', params: 1 },
})
.then((response) => console.log('message received from worker: ', response.msg));
對于 worker 部分的處理就簡單得多,計算處理完畢后,在回應回復中帶上請求的 id 即可,
/* worker.js */
class WorkerThreadController {
constructor() {
this.worker = self;
// 等待異步回呼集合
this.actionHandlerMap = {};
this.worker.onmessage = this.onmessage.bind(this);
}
async onmessage(e) {
const { id, actionType, payload } = e.data;
switch (actionType) {
case 'print':
console.log(payload.msg);
self.postMessage({ id, response: { msg: 'msg has been print.' } });
break;
case 'asyncCalc':
// 構造一個異步處理情形
const result = await new Promise((resolve) => setTimeout(() => resolve(payload.params * 2), 1000));
self.postMessage({ id, response: { msg: `the caculated answer is ${result}.` } });
break;
default:
break;
}
}
}
const workerThreadController = new WorkerThreadController();
當然,worker 這邊的改造還能夠更進一步,我們發現,當 Worker 需要接收的計算種類增多,使用 switch 方式包裹的 onmessage 函式就會變得冗長,使用字串判斷也不夠可靠,我們可以用策略模式簡單地封裝一下 Worker 中的邏輯,
/* worker.js */
// 可以單獨抽成一個檔案,然后 import 進來
const api = {
print(payload) {
console.log(payload.msg);
return { msg: 'msg has been print.' };
},
async asyncCalc(payload) {
const result = await new Promise((resolve) => setTimeout(() => resolve(payload.params * 2), 1000));
return { msg: `the caculated answer is ${result}.` };
},
};
class WorkerThreadController {
constructor() {
this.worker = self;
// 等待異步回呼集合
this.actionHandlerMap = {};
this.worker.onmessage = this.onmessage.bind(this);
}
async onmessage(e) {
const { id, actionType, payload } = e.data;
const result = await api[actionType].call(this, payload);
self.postMessage({ id, response: result });
}
}
const workerThreadController = new WorkerThreadController();
至此,一個簡單好用的 Promise Worker 就建立完成了,
當然,為了增加框架的魯棒性,我們還應該加入類似于錯誤處理,報錯及監控資料上報等等能力,由于不屬于本文探討的范圍,這里就先按住不表,有興趣的讀者可以參看 AlloyTeam 最新開源的 alloy-worker 專案,其中對上述存在的問題進行了全面的補足,是一個較為完善的高可用的 Worker 通信框架,
總結
本文對 Web Worker 進行了簡要的介紹,包括其能力以及局限性,讓讀者對 Worker 的使用場景有一個全面的了解,提出了一種封裝 Worker 原生 API 使之能被 Promise 化呼叫的解決方案,并在最后推薦了團隊內正在使用的功能完善的成熟解決方案,希望能幫助到近期有興趣進行 Worker 改造的前端開發者們,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/345767.html
標籤:其他
上一篇:軟體工程專案“海大學舍”Scrum Meeting-1
下一篇:java版Spring Cloud+SpringBoot+mybatis+uniapp b2b2c 多商戶入駐商城 直播 電子商務之全渠道資料庫高可用
