hello~親愛的看官老爺們大家好~最近入職新公司後,接手視覺化平臺的開發工作。經過收集意見後,發現使用者普遍吐槽渲染時太卡。檢視介面後發現,後端直接返回查詢資料庫後的原始結果,需要頁端傻乎乎地迴圈處理各種資料。當資料量大的時候,使用者就只能傻看著瀏覽器卡死。
優化方案相信大家都有想法,在無法改後端資料體的情況下,最低成本的當然是引入 Web Worker
,將處理邏輯丟給它就好。但由於各種原因,建一個 Web Worker
檔案再求後端爸爸部署,暫時是不太可行的。那麼,腦洞大開想一想,能不能頁端動態建立一個呢?
於是,提出瞭如下問題:
- 是否能動態建立
Web Worker
? - 如能建立的話,希望
API
呼叫是物件導向風格,而且支援Pormise
呼叫。 - 為節省資源起見,這個動態建立的
Web Worker
希望是可複用的。
帶著這些問題,開始瞎折騰之旅吧!
動態建立 Web Worker
Web Worker
是什麼,相信大家都瞭然於胸,一般的引用方法是請求一個 JS
檔案,即:
const worker = new Wokrer(worker's url);
複製程式碼
因而,問題可以轉化為,我們如何將一個函式,轉換成瀏覽器可識別並能重新解析為該函式的 url 呢?之前看 iframe和HTML5 blob實現JS,CSS,HTML直接當前頁預覽這篇文章時,接觸到 URL.createObjectURL()
,文中對此的描述是:“使用 URL.createObjectURL()
方法將 Blob
物件轉換為 URL
物件並賦予我們建立的 iframe
元素的src屬性。”那麼,是否能將其用於建立 Web Worker
的 url
呢?查閱 MDN
文件後發現了這麼一句:
Note: 此特性在 Web Worker 中可用。
喜大普奔,計劃通!該 API
接受 File
物件或者 Blob
物件作為引數,此處我們使用 Blob
物件作為引數並指定型別為 text/javascript
,測試 demo
如下:
function demo() {
setTimeout(() => {
postMessage('success!');
}, 1000)
}
const blob = new Blob([demo.toString() + ' demo()'], { type: 'text/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.addEventListener('message', function(res) {
console.log(res);
})
複製程式碼
開啟 Chrome
,在控制檯中輸入上面的程式碼,隔一秒之後便能列印出:MessageEvent {isTrusted: true, data: "success!", origin: "", lastEventId: "", source: null, …}
。實驗通過,可以確認 Web Worker
能被動態建立。
封裝 API
既然能動態建立,作為程式設計師,肯定是不希望每次都手動建立,因而封裝一下以便於使用。由於可能會多處使用,因而封裝成一個“類”,有助於程式碼複用與節省記憶體。建構函式接受一個函式作為引數即可,這個“類”應有一個 send
方法讓我們再想傳送資料時呼叫。那麼基礎的架子應該是:
class DynamicWorker {
constructor(cb) {
//根據 cb 建立 Web Worker
}
send(data) {
//傳入引數後傳送資料給 Web Worker,Web Worker處理後返回
}
}
複製程式碼
鑑於創造的 Web Worker
不是馬上就處理資料,而是在 DynamicWorker
例項呼叫 send
方法時才開始幹活,因而不能像上面的 demo
一樣直接寫死 postMessage
,而應該多構建一個 onmessage
函式,因而 constructor
函式構建如下:
constructor(cb) {
const _fn = `const _fn = ${cb.toString()};`;
const _handle = ` onmessage = function ({ data }) {
postMessage(_fn(data));
}`;
const blob = new Blob([_fn + _handle], { type: 'text/javascript' });
this.worker = new Worker(URL.createObjectURL(blob));
//釋放被引用的 url 物件,經過測試,就算釋放了也能重複訪問建立的 Web Worker的
URL.revokeObjectURL(blob);
}
複製程式碼
稍微需要解釋下的是為何需要在 Web Worker
中創造一個 _fn
變數,這是由於生成 Blob
物件時,接受的引數是字串陣列,如果只是 cb.toString()
的話,是拿不到函式名的,那麼在 Web Worker
中執行該函式更是無從談起,因而賦值一個變數,在 message
事件觸發後使用 postMessage
返回函式處理後的結果。
之後就是 send
方法了,該方法返回一個 Promise
,當接收到 Web Worker
傳回的資料後,改變 Promise
的狀態。根據這個思路,有這樣的設計:
send(data) {
const worker = this.worker;
let resolve = null;
function _handleResult({ data }) {
resolve(data);
}
worker.addEventListener('message', _handleResult);
worker.postMessage(data);
return new Promise((res) => {
resolve = res;
})
}
複製程式碼
根據思路看程式碼,其實是很好理解的。複製 DynamicWorker
的程式碼丟去控制檯,執行下面的程式碼:
const test = new DynamicWorker(function(data) {
return data;
})
test.send(123).then(res => console.log(res));
複製程式碼
就能愉快地看到瀏覽器列印出 123
了。
複用與優化
上述的程式碼基本能達到我們的預期,但存在若干問題,如果短時間內呼叫多次 send
方法,那麼後呼叫的方法會得出前面前面的結果。這是由於 worker.addEventListener('message', _handleResult)
區分不出每次呼叫,因而收到訊息後一律執行 resolve
,大家應該都知道 Promise
的狀態決議後就無法再修改的,因而導致呼叫錯誤。此處可以通過新增一個標誌位解決。另一個問題是潛藏的,為 worker
新增太多的事件監聽器了,其實這大可不必,一個 DynamicWorker
例項一個事件監聽器就足夠了。結合這兩點,修改一下對應的程式碼,首先是 constructor
:
...
const _handleResult = ({ data: { data, flag } }) => {
const _res = this._map[flag];
if (_res) {
_res(data);
this._map[flag] = null;
}
}
this._map = {};
this.worker.addEventListener('message', _handleResult);
...
複製程式碼
為其添上這幾行程式碼即可,通過 _map
快取不同的標誌,而根據不同的標誌可以區分出不同的 Promise
,根據標誌位對應的值呼叫即可。同理,send
方法也需要作出修改,為其新增標誌的生成,完整的 DynamicWorker
程式碼如下:
class DynamicWorker {
constructor(cb) {
const _fn = `const _fn = ${cb.toString()};`;
const _handle = ` onmessage = function ({ data: { data, flag } }) {
postMessage({
data: _fn(data),
flag
});
}`;
const _handleResult = ({ data: { data, flag } }) => {
const _res = this._map[flag];
if (_res) {
_res(data);
this._map[flag] = null;
}
}
const blob = new Blob([_fn + _handle], { type: 'text/javascript' });
this.worker = new Worker(URL.createObjectURL(blob));
this._map = {};
this.worker.addEventListener('message', _handleResult);
URL.revokeObjectURL(blob);
}
send(data) {
const worker = this.worker;
const flag = Math.random();
worker.postMessage({
data,
flag,
});
return new Promise((res) => {
this._map[flag] = res;
})
}
}
複製程式碼
還是挺簡單易懂的對不對?呼叫 API
是沒有變化的,優化的只是內部的邏輯,有興趣的同學可以複製進去控制檯,隨意寫個需要運算很久的函式丟進去,嘗試下瀏覽器是否不會卡住哦~
小結
這個 DynamicWorker
還只是未成品,缺少錯誤處理,非同步處理等功能。而且應該有同學可能會懷疑這樣的東西到底有何用途,相容性上也有不少問題,當時我寫完程式碼後也有這個疑問(因為下個月就會接入 node.js
,可以自己部署檔案)。思考後認為,對比正常使用 Web Worker
,動態建立最大的優點是在於動態,不依賴於伺服器部署檔案,相對靈活易用。對於某些需要依賴運算大量資料,但又無法控制伺服器的場景來說,不失為一個可行的解決思路。
最後,有個問題想請教一下,在需要處理大量資料的場景下,node.js
也不是一個好的選擇,只是因為頁端效能差與只是內部系統的關係,將處理邏輯上移到 node.js
,那麼正確的處理方式或者架構是怎樣的呢?還望有相關經驗的同學不吝賜教。
感謝各位看官大人看到這裡,知易行難,希望本文對你有所幫助~謝謝!