摘要: 理解Web Workers。
Fundebug經授權轉載,版權歸原作者所有。
這是專門探索 JavaScript 及其所構建的元件的系列文章的第7篇。
如果你錯過了前面的章節,可以在這裡找到它們:
- JavaScript是如何工作的:引擎,執行時和呼叫堆疊的概述!
- JavaScript是如何工作的:深入V8引擎&編寫優化程式碼的5個技巧!
- JavaScript如何工作:記憶體管理+如何處理4個常見的記憶體洩漏 !
- JavaScript是如何工作的:事件迴圈和非同步程式設計的崛起+ 5種使用 async/await 更好地編碼方式!
- JavaScript是如何工作: 深入探索 websocket 和HTTP/2與SSE +如何選擇正確的路徑!
- JavaScript是如何工作的:與 WebAssembly比較 及其使用場景 !
這次我們會逐步講解 Web Workers,先說個簡單的概念,接著討論不同型別的 Web Workers,他們的組成部分是如何一起工作的,以及不同場景下它們各自優勢和限制。最後,提供5個正確使用 Web Workers 的場景。
正如我們前面文章討論的那樣,你應該知道 JavaScript 語言採用的是單執行緒模型。然而,JavaScript 也為開發人員提供了編寫非同步程式碼的機會。
非同步程式設計的侷限性
以前的文章討論過非同步程式設計,以及應該在什麼時候使用它。
非同步程式設計可以讓UI介面是響應式(渲染速度快)的,通過"程式碼排程",讓需要請求時間的程式碼先放到在 event loop中晚一點再執行,這樣就允許UI先行渲染展示。
非同步程式設計的一個很好的用例就 AJAX 請求。由於請求可能花費大量時間,因此可以使用非同步請求,在客戶端等待響應的同時還可以執行其他程式碼。
然而,這帶來了一個問題——請求是由瀏覽器的WEB API處理的,但是如何使其他程式碼是非同步的呢?例如,如果成功回撥中的程式碼非常佔用CPU:
var result = performCPUIntensiveCalculation();
複製程式碼
如果 performCPUIntensiveCalculation
不是一個HTTP請求而是一個阻塞程式碼(比如一個內容很多的for loop迴圈),就沒有辦法及時清空事件迴圈,瀏覽器的 UI 渲染就會被阻塞,頁面無法及時響應給使用者。
這意味著非同步函式只能解決一小部分 JavaScript 語言單執行緒中的侷限性問題。
在某些情況下,可以使用 setTimeout
對長時間執行的計算阻塞的,可以使用 setTimeout
暫時放入非同步佇列中,從讓頁面得到更快的渲染。例如,通過在單獨的 setTimeout
呼叫中批處理複雜的計算,可以將它們放在事件迴圈中單獨的“位置”上,這樣可以爭取為 UI 渲染/響應的執行時間。
看一個簡單的函式,計算一個數字陣列的平均值:
以下是重寫上述程式碼並“模擬”非同步性的方法:
function averageAsync(numbers, callback) {
var len = numbers.length,
sum = 0;
if (len === 0) {
return 0;
}
function calculateSumAsync(i) {
if (i < len) {
// Put the next function call on the event loop.
setTimeout(function() {
sum += numbers[i];
calculateSumAsync(i + 1);
}, 0);
} else {
// The end of the array is reached so we're invoking the callback.
callback(sum / len);
}
}
calculateSumAsync(0);
}
複製程式碼
使用setTimeout函式,該函式將在事件迴圈中進一步新增計算的每個步驟。在每次計算之間,將有足夠的時間進行其他計算,從而可以讓瀏覽器進行渲染。
Web Worker 可以解決這個問題
HTML5為我們帶來了很多新的東西,包括:
- SSE(我們在前一篇文章中已經描述並與WebSockets進行了比較)
- Geolocation
- Application cache
- Local Storage
- Drag and Drop
- Web Workers
Web Worker 概述
Web Worker 的作用,就是為 JavaScript 創造多執行緒環境,允許主執行緒建立 Worker 執行緒,將一些任務分配給後者執行。在主執行緒執行的同時,Worker 執行緒在後臺執行,兩者互不干擾。等到 Worker 執行緒完成計算任務,再把結果返回給主執行緒。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 執行緒負擔了,主執行緒(通常負責 UI 互動)就會很流暢,不會被阻塞或拖慢。
你可能會問:“JavaScript不是一個單執行緒的語言嗎?”
事實上 JavaScript 是一種不定義執行緒模型的語言。Web Workers 不是 JavaScript 的一部分,而是可以通過 JavaScript 訪問的瀏覽器特性。歷史上,大多數瀏覽器都是單執行緒的(當然,這已經改變了),大多數 JavaScript 實現都入發生在瀏覽器中。Web Workers 不是在 Node.JS 中實現的。Node.js 中有類似的叢集(cluster)、子程式概念(child_process),他們也是多執行緒但是和 Web Workers 還是有區別 。
值得注意的是,規範 中提到了三種型別的 Web Workers:
Dedicated Workers
專用 Workers 只能被建立它的頁面訪問,並且只能與它通訊。以下是瀏覽器支援的情況:
Shared Workers
共享 Workers 在同一源(origin)下面的各種程式都可以訪問它,包括:iframes、瀏覽器中的不同tab頁(一個tab頁就是一個單獨的程式,所以Shared Workers可以用來實現 tab 頁之間的交流)、以及其他的共享 Workers。以下是瀏覽器支援的情況:
Service workers
Service Worker 功能:
- 後臺訊息傳遞
- 網路代理,轉發請求,偽造響應
- 離線快取
- 訊息推送
在目前階段,Service Worker 的主要能力集中在網路代理和離線快取上。具體的實現上,可以理解為 Service Worker 是一個能在網頁關閉時仍然執行的 Web Worker。以下是瀏覽器支援的情況:
本文主要討論 專用 Workers,沒有特別宣告的話,Web Workers、Workers都是指代的專用 Workers。
Web Workers 是如何工作
Web Workers 一般通過指令碼為 .js
檔案來構建,在頁面中還通過了一些非同步的 HTTP 請求,這些請求是完全被隱藏了的,你只需要呼叫 Web Worker API.
Worker 利用類執行緒間訊息傳遞來實現並行性。它們保證介面的實時性、高效能和響應性呈現給使用者。
Web Workers 在瀏覽器中的一個獨立執行緒中執行。因此,它們執行的程式碼需要包含在一個單獨的檔案中。這一點很重要,請記住!
讓我們看看基本 Workers 是如何建立的:
var worker = new Worker('task.js');
複製程式碼
Worker()
建構函式的引數是一個指令碼檔案,該檔案就是 Worker 執行緒所要執行的任務。由於 Worker 不能讀取本地檔案,所以這個指令碼必須來自網路。如果下載沒有成功(比如404錯誤),Worker 就會默默地失敗。
為了啟動建立的 Worker,需要呼叫 postMessage
方法:
worker.postMessage();
複製程式碼
Web Worker 通訊
為了在 Web Worker 和建立它的頁面之間進行通訊,需要使用 postMessage
方法或 Broadcast Channel。
postMessage 方法
新瀏覽器支援JSON物件作為方法的第一個引數,而舊瀏覽器只支援字串。
來看一個示例,通過將 JSON 物件作為一個更“複雜”的示例傳遞,建立 Worker 的頁面如何與之通訊。傳遞字串跟傳遞物件的方式也是一樣的。
讓我們來看看下面的 HTML 頁面(或者更準確地說是它的一部分):
<button onclick="startComputation()">Start computation</button>
<script>
function startComputation() {
worker.postMessage({'cmd': 'average', 'data': [1, 2, 3, 4]});
}
var worker = new Worker('doWork.js');
worker.addEventListener('message', function(e) {
console.log(e.data);
}, false);
</script>
複製程式碼
然後這是 worker 中的 js 程式碼:
self.addEventListener('message', function(e) {
var data = e.data;
switch (data.cmd) {
case 'average':
var result = calculateAverage(data); // 從數值陣列中計算平均值的函式
self.postMessage(result);
break;
default:
self.postMessage('Unknown command');
}
}, false);
複製程式碼
當單擊該按鈕時,將從主頁呼叫 postMessage
。postMessage 行將 JSON 物件傳給Worker。Worker 通過定義的訊息處理程式監聽並處理該訊息。
當訊息到達時,實際的計算在worker中執行,而不會阻塞事件迴圈。Worker 檢查傳遞的事件引數 e
,像執行 JavaScript 函式一樣,處理完成後,把結果傳回給主頁。
在 Worker 作用域中,this 和 self 都指向 Worker 的全域性作用域。
有兩種方法可以停止 Worker:從主頁呼叫
worker.terminate()
或在worker
內部呼叫self.close()
。
Broadcast Channel
Broadcast Channel API 允許同一原始域和使用者代理下的所有視窗,iFrames 等進行互動。也就是說,如果使用者開啟了同一個網站的的兩個標籤視窗,如果網站內容發生了變化,那麼兩個視窗會同時得到更新通知。
還是不明白?就拿 Facebook 作為例子吧,假如你現在已經開啟 了Facebook 的一個視窗,但是你此時還沒有登入,此時你又開啟另外一個視窗進行登入,那麼你就可以通知其他視窗/標籤頁去告訴它們一個使用者已經登入了並請求它們進行相應的頁面更新。
// Connection to a broadcast channel
var bc = new BroadcastChannel('test_channel');
// Example of sending of a simple message
bc.postMessage('This is a test message.');
// Example of a simple event handler that only
// logs the message to the console
bc.onmessage = function (e) {
console.log(e.data);
}
// Disconnect the channel
bc.close()
複製程式碼
可以從下面這張圖,在視覺上來清晰地感受 Broadcast Channel:
Broadcast Channel 瀏覽器支援比較有限:
訊息的大小
有兩種方式傳送訊息給Web Workers:
- 複製訊息:訊息被序列化、複製、傳送,然後在另一端反序列化。頁面和 Worker 不共享相同的例項,因此最終的結果是每次傳遞都會建立一個副本大多數瀏覽器,在兩邊都是使用的JSON對值進行編碼和解碼,這樣對資料的解碼、編碼操作,勢必會增加訊息傳輸過程的時間開銷。資訊越大,傳送的時間就越長。
- 傳遞訊息:這意味著原始傳送方在一旦傳送後不能再使用它。傳輸資料幾乎是瞬間的,這種傳輸方式的侷限性在於只能用 ArrayBuffer 型別來傳遞。
Web Workers 可用的特性
由於 JavaScript的多執行緒特性,Web工作者只能訪問JavaScript特性的一個子集。以下是它的一些特點:
Web Workers 由於具有多執行緒特性,因此只能訪問 JavaScript 特性的子集。 以下是可使用特性列表:
- navigator 物件
- location 物件(只讀)
- MLHttpRequest
- setTimeout()/clearTimeout() and setInterval()/clearInterval()
- 應用快取(Application Cache)
- 使用
importScripts()
匯入外部指令碼 - 建立其他的 Web Workers
Web Workers 的侷限性
遺憾的是,Web Workers 無法訪問一些非常關鍵的 JavaScript 特性:
- DOM(它會造成執行緒不安全)
- window 物件
- document 物件
- parent 物件
這意味著 Web Worker 不能操作 DOM (因此也不能操作 UI)。有時這可能很棘手,但是一旦你瞭解瞭如何正確使用 Web Workers,你就會開始將它們作為單獨的“計算機”使用,而所有 UI 更改都將發生在你的頁面程式碼中。 Workers 將為你完成所有繁重的工作,然後一旦完成再把結果返回給 page 頁面。
處理錯誤
和 JavaScript 程式碼一樣,Web workers 裡丟擲的錯誤,你也需要進行處理。當 Worker 執行過程中如果遇到錯誤,會觸發一個 ErrorEvent
事件。介面包含了三個有用的屬性來幫忙排查問題:
- filename - 導致 Worker 的指令碼名稱
- lineno - 發生錯誤的行號
- message - 對錯誤的描述
例子如下:
在這裡,可以看到我們建立了一個 worker 並開始偵聽錯誤事件。
在 worker 內部(在 workerWithError.js
中),我們通過將未定義 x
乘以 2 來建立一個異常。異常被傳播到初始指令碼,然後通過頁面監聽 error事件,對錯誤進行捕獲。
5個好的 Web Workers 應用例項
到目前為止,我們已經列出了 Web Workers 的優點和侷限性。現在讓我們看看它們最強大的用例是什麼:
- Ray tracing(光線追蹤):光線追蹤是一種以畫素為單位跟蹤光的路徑生成影象的渲染技術。光線追蹤利用 CPU 密集型的數學計算來模擬光的路徑。其思想是模擬一些效果,如反射、折射、材料等。所有這些計算邏輯都可以新增到 Web Worker 中,以避免阻塞 UI執行緒。更好的是——可以很容易地在多個 workers 之間(以及在多個cpu之間)分割影象呈現。下面是一個使用 Web Workers 的光線追蹤的簡單演示—nerget.com/rayjs-mt/r.…。
- **Encryption(加密):**由於對個人和敏感資料的監管越來越嚴格,端到端加密越來越受歡迎。加密是一件非常耗時的事情,特別是如果有很多資料需要頻繁加密(例如,在傳送到伺服器之前)。這是一個使用 Web Worker 非常好的場景,因為它不需要訪問 DOM 或任何花哨的東西——它是完成其工作的純演算法。只要是在 Web Worker 中工作的,對於端使用者就是無縫的,不會影響到體驗。
- **Prefetching data(預取資料):**為了優化你的網站或 web 應用程式並改進資料載入時間,你可以利用 Web Workers 提前載入和儲存一些資料,以便在需要時稍後使用。Web Workers 在這種情況下非常棒,因為它們不會影響應用程式的UI,這與不使用Workers 時是不同的。
- **Progressive Web Apps(漸進式Web應用程式):**這種漸進式Web應用程式要求,即使在使用者網路不穩定的條件下,也能夠迅速的載入。這意味著資料必須本地儲存在瀏覽器中。這也是 IndexDB 或類似 api 發揮作用的地方。通常情況下,客戶端的儲存都是必要的,但使用起來需要不阻塞UI渲染執行緒,那麼工作就需要在 Worker 中進行了。不過,以IndexDB 為例,它提供了一些非同步的API,呼叫它們的話也不需要使用 web worker,但如果是同步的 API,就必須要在 Worker 中使用了。
- **Spell checking(拼寫檢查):**一個基本的拼寫檢查程式的工作流程如下-程式讀取一個字典檔案與一個正確拼寫單詞列表。字典被解析為一個搜尋樹,以使實際的文字搜尋更有效。當一個單詞被提供給檢查器時,程式檢查它是否存在於預先構建的搜尋樹中。如果在樹中沒有找到該單詞,可以通過替換替換字元並測試它是否是有效的單詞(如果是使用者想要寫的單詞),為使用者提供替代拼寫。所有的這些處理過程都可以在 Web Worker中進行了,使用者可以不被阻塞的輸入詞彙和句子,Web Worker 在後臺校驗詞彙是否正確以及提供備選詞彙。
原文:
程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具Fundebug。
你的點贊是我持續分享好東西的動力,歡迎點贊!
一個笨笨的碼農,我的世界只能終身學習!
更多內容請關注公眾號《大遷世界》!
關於Fundebug
Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java線上應用實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了9億+錯誤事件,付費客戶有Google、360、金山軟體、百姓網等眾多品牌企業。歡迎大家免費試用!