[譯] JavaScript 工做原理:Web Worker 的內部構造以及 5 種你應當使用它的場景

這是探索 JavaScript 及其內建組件系列文章的第 7 篇。在認識和描述這些核心元素的過程當中,咱們也會分享咱們在構建 SessionStack 時所遵循的一些經驗規則。SessionStack 是一個輕量級 JavaScript 應用,它協助用戶實時查看和復現他們的 Web 應用缺陷,所以其自身不只須要足夠健壯還要有不俗的性能表現。javascript

若是你錯過了前面的文章,你能夠在下面找到它們:html

這一次咱們將剖析 Web Worker:對它進行簡單概述後,咱們將分別討論不一樣類型的 Worker 以及它們內部組件的運做方法,同時也會以場景爲例說明它們各自的優缺點。在文章的最後,咱們將講解最適合使用 Web Worker 的 5 個場景。前端

咱們在 以前的文章 中已經詳盡地討論了 JavaScript 的單線程運行機制,對此你應當已經瞭然於胸。然而,JavaScript 是容許開發者在單線程模型上書寫異步代碼的。html5

異步編程的 「天花板」

咱們已經討論過了 異步編程 的概念及其使用場景。java

異步編程經過把部分代碼 「放置」 到事件循環較後的時間點執行,保證了 UI 渲染始終處於較高的優先級,這樣你的 UI 就不會出現卡頓無響應的狀況。android

AJAX 請求是異步編程的最佳實踐之一。一般網絡請求不會在短期內獲得響應,所以異步的網絡請求能讓客戶端在等待響應結果的同時執行其餘業務代碼。ios

// 假設你使用了 jQuery
jQuery.ajax({
    url: 'https://api.example.com/endpoint',
    success: function(response) {
        // 正確響應後須要執行的代碼
    }
});
複製代碼

固然這裏有個問題,上例可以進行異步請求是依靠了瀏覽器提供的 API,其餘代碼又該如何實現異步執行呢?例如,在上例 success 回調函數中存在 CPU 密集型計算:git

var result = performCPUIntensiveCalculation();
複製代碼

假如 performCPUIntensiveCalculation 不是一個 HTTP 請求,而是一段能夠阻塞線程的代碼(例:一段巨型 for 循環代碼)。這樣會使 event loop 不堪重負,瀏覽器 UI 也隨之阻塞 —— 用戶將面對卡頓無響應的網頁。github

這就說明了使用異步函數只能解決 JavaScript 單線程模型帶來的一小部分問題。web

在一些因大量計算引發的 UI 阻塞問題中,使用 setTimeout 來解決阻塞的效果還不錯。例如,咱們能夠把一系列的複雜計算分批放到單獨的 setTimeout 中執行,這樣作等因而把連續的計算分散到了 event loop 中的不一樣位置,以此爲 UI 的渲染和事件響應讓出了時間。

讓咱們來看一個簡單的計算數組均值的函數:

function average(numbers) {
    var len = numbers.length,
        sum = 0,
        i;

    if (len === 0) {
        return 0;
    } 
    
    for (i = 0; i < len; i++) {
        sum += numbers[i];
    }
   
    return sum / len;
}
複製代碼

下面是對上方代碼的一個重寫,使其得到了異步性:

function averageAsync(numbers, callback) {
    var len = numbers.length,
        sum = 0;

    if (len === 0) {
        return 0;
    } 

    function calculateSumAsync(i) {
        if (i < len) {
            // 把下一次函數調用放入 event loop
            setTimeout(function() {
                sum += numbers[i];
                calculateSumAsync(i + 1);
            }, 0);
        } else {
            // 計算完數組中全部元素後,調用回調函數返回結果
            callback(sum / len);
        }
    }

    calculateSumAsync(0);
}
複製代碼

經過使用 setTimeout 能夠把每一步計算都放置到 event loop 較後的時間點執行。在每兩次的計算間隔,event loop 便會有足夠的時間執行其餘計算,從而保證瀏覽器不會一 」凍「 不動。

拯救你於水火之中的 Web Worker

HTML5 已經提供了很多開箱即用的好東西,包括:

  • SSE (在 上一篇文章 中已經談過它的特性並與 WebSocket 進行了對比)
  • 地理信息
  • 應用緩存
  • LocalStorage
  • 拖放手勢
  • Web Worker

Web Worker 是內建在瀏覽器中的輕量級 線程,使用它執行 JavaScript 代碼不會阻塞 event loop。

很是神奇吧,原本 JavaScript 中的全部範例都是基於單線程模型實現的,但這裏的 Web Worker 卻(在必定程度上)突破了這一限制。

今後開發者能夠遠離 UI 阻塞的困擾,經過把一些執行時間長、計算密集型的任務放到後臺交由 Web Worker 完成,使他們的應用響應變得更加迅速。更重要的是,咱們不再須要對 event loop 施加任何的 setTimeout 黑魔法。

這裏有一個簡單的數組排序 demo ,其中對比了使用 Web Worker 和不使用 Web Worker 時的區別。

Web Worker 概覽

Web Worker 容許你在執行大量計算密集型任務時,還不阻塞 UI 進程。事實上,兩者互不不阻塞的緣由就是它們是並行執行的,能夠看出 Web Worker 是貨真價實的多線程。

你可能想說 — 」JavaScript 不是一個在單線程上執行的語言嗎?「。

你可能會驚訝 JavaScript 做爲一門編程語言,卻沒有定義任何的線程模型。所以 Web Worker 並不屬於 JavaScript 語言的一部分,它僅僅是瀏覽器提供的一項特性,只是它能夠被 JavaScript 訪問、調用罷了。過往的衆多瀏覽器都是單線程程序(之前的理所固然,如今也有了些許變化),而且瀏覽器一直以來也是 JavaScript 主要的運行環境。對比在 Node.JS 中就沒有 Web Worker 的相關實現 — 雖然 Web Worker 對應着 Node.JS 中的 「cluster」 或 「child_process」 概念,不過它們仍是有所區別的。

值得注意的是,Web Worker 的 定義 中一共包含了 3 種類型的 Worker:

Dedicated Worker(專用 Worker)

Dedicated Worker 由主線程實例化且只能與它通訊。

Dedicated Worker 瀏覽器兼容性一覽

Shared Worker(共享 Worker)

Shared Worker 能夠被同一域(瀏覽器中不一樣的 tab、iframe 或其餘 Shared Worker)下的全部線程訪問。

Shared Worker 瀏覽器兼容一覽

Service Worker(服務 Worker)

Service Worker 是一個事件驅動型 Worker,它的初始化註冊須要網頁/站點的 origin 和路徑信息。一個註冊好的 Service Worker 能夠控制相關網頁/網站的導航、資源請求以及進行粒度化的資源緩存操做,所以你能夠極好地控制應用在特定環境下的表現(如:無網絡可用時)。

Service Worker 瀏覽器兼容一覽

在本文中,咱們主要討論 Dedicated Worker,後文的 」Web Worker「 或 「Worker」 都默認指代它。

Web Worker 工做原理

最終實現 Web Worker 的是一堆 .js 文件,網頁會經過異步 HTTP 請求來加載它們。固然 Web Worker API 已經包辦了這一切,上述加載對使用者徹底無感。

Worker 利用相似線程的消息機制保持了與主線程的平行,它是提高你應用 UI 體驗的不二人選,使用 Worker 保證了 UI 渲染的實時性、高性能和快速響應。

Web Worker 是運行在瀏覽器內部的一條獨立線程,所以須要使用 Web Worker 運行的代碼塊也必須存放在一個 獨立文件 中。這一點須要牢記在心。

讓咱們看看,如何建立一個基礎 Worker:

var worker = new Worker('task.js');
複製代碼

若是此處的 「task.js」 存在且能被訪問,那麼瀏覽器會建立一個新的線程去異步地下載源代碼文件。一旦下載完成,代碼將馬上執行,此時 Worker 也就開始了它的工做。 若是提供的代碼文件不存在返回 404,那麼 Worker 會靜默失敗並不拋出異常。

爲了啓動建立好的 Worker,你須要顯式地調用 postMessage 方法:

worker.postMessage();
複製代碼

Web Worker 通訊

爲了使建立好的 Worker 和建立它的頁面可以通訊,你須要使用 postMessage 方法或 Broadcast Channel(廣播通道).

使用 postMessage 方法

在較新的瀏覽器中,postMessage 方法支持 JSON 對象做爲函數的第一個入參,可是在舊版本瀏覽器中它仍是隻支持 string

下面的 demo 會展現 Worker 是如何與建立它的頁面進行通訊的,同時咱們將使用 JSON 對象做爲通訊體好讓這個 demo 看起來稍微 「複雜」 一點。若改成傳遞字符串,方法也不言而喻了。

讓咱們看看下面的 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 腳本中的內容:

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);
複製代碼

當主頁面中的 button 被按下,觸發調用了 postMessage 方法。worker.postMessage 這行代碼會傳遞一個 JSON 對象給 Worker,對象中包含了 cmddata 兩個鍵以及它們對應的值。相應的,Worker 會經過定義的 message 響應方法拿到和處理上面傳遞過來的消息內容。

當消息到達 Worker 後,實際的計算便開始運行,這樣徹底不會阻塞 event loop。在此過程當中,Worker 只會檢查傳遞來的事件 e,而後像往常執行 JavaScript 函數同樣繼續執行。當最終執行完成,執行結果會回傳回主頁面。

在 Worker 的執行上下文中,selfthis 都指向 Worker 的全局做用域。

有兩種中止 Worker 的方法:一、在主頁面中顯示地調用 worker.terminate() ;二、在腳本中調用 self.close() 讓 Worker 自行了斷。

Broadcast Channel(廣播通道)

Broadcast Channel 是更純粹地爲通訊而生的 API。它容許咱們在同域下的全部的上下文中發送和接收消息,包括瀏覽器 tab、iframe 和 Worker:

// 建立一個到 Broadcast Channel 的鏈接
var bc = new BroadcastChannel('test_channel');

// 發送一段簡單的消息
bc.postMessage('This is a test message.');

// 這是一個簡單的事件 handler
// 咱們會在 handler 中接收並打印消息到終端
bc.onmessage = function (e) { 
  console.log(e.data); 
}

// 斷開與 Broadcast Channel 的鏈接
bc.close()
複製代碼

下圖會幫助你理解 Broadcast Channel 的工做原理:

使用 Broadcast Channel 會有更嚴格的瀏覽器兼容限制:

消息的大小

一共有 2 種給 Web Worker 發送消息的方法:

  • 拷貝消息: 這種方法下消息會被序列化、拷貝而後再發送出去,接收方接收後則進行反序列化取得消息。所以上例中的頁面和 Worker 不會共享同一個消息實例,它們之間每發送一次消息就會多建立一個消息副本。大多數瀏覽器都採用這樣的發送方法,而且會在發送和接收端自動進行 JSON 編碼/解碼。如你所預料的,這些數據處理會給消息傳送帶來不小的負擔。傳送的消息越大,時間開銷就越大。
  • 傳遞消息: 使用這種方法意味着消息發送者一旦成功發送消息後,就再也沒法使用發出的消息數據了。消息的傳送幾乎不耗費任什麼時候間,美中不足的是隻有 ArrayBuffer 支持以這種方式發送。

Web Worker 中支持的 JavaScript 特性

由於 Web Worker 的多線程天性使然,它只能使用 一小撮 JavaScript 提供的特性,列表以下:

  • navigator 對象
  • location 對象(只讀)
  • XMLHttpRequest
  • setTimeout()/clearTimeout()setInterval()/clearInterval()
  • 應用緩存
  • 使用 importScripts() 引入外部 script
  • 建立其餘的 Web Worker

Web Worker 的侷限性

使人遺憾的是 Web Worker 沒法訪問一些很是重要的 JavaScript 特性:

  • DOM 元素(訪問不是線程安全的)
  • window 對象
  • document 對象
  • parent 對象

這意味着 Web Worker 不能作任何的 DOM 操做(也就是 UI 層面的工做)。剛開始這會顯得略微棘手,不過一旦你學會了如何正確使用 Web Worker。你就只會把 Web Worker 用做單獨的 」計算機器「,而把全部的 UI 操做放到頁面代碼中。你能夠把全部的髒活累活都交給 Web Worker 完成,再將它勞做的結果傳到頁面並在那裏進行必要的 UI 操做。

異常處理

像對待任何 JavaScript 代碼同樣,你但願處理 Web Worker 拋出的任何錯誤。當 Worker 在運行時發生錯誤,它會觸發 ErrorEvent 事件。該接口包含 3 個有用的屬性,它們能幫助你定位代碼出錯的緣由:

  • filename - 發生錯誤的 script 文件名
  • lineno - 發生錯誤的代碼行號
  • message - 錯誤信息

這有一個例子:

function onError(e) {
  console.log('Line: ' + e.lineno);
  console.log('In: ' + e.filename);
  console.log('Message: ' + e.message);
}

var worker = new Worker('workerWithError.js');
worker.addEventListener('error', onError, false);
worker.postMessage(); // 不傳遞消息僅啓動 Worker
複製代碼
self.addEventListener('message', function(e) {
  postMessage(x * 2); // 此行故意使用了未聲明的變量 'x'
};
複製代碼

能夠看到,咱們在這兒建立了一個 Worker 並監聽着它發出的 error 事件。

經過使用一個在做用域內未定義的變量 x 做乘法,咱們在 Worker 內部(workerWithError.js 文件內)故意製造了一個異常。這個異常會被傳遞到最初建立 Worker 的 scrpit 中,同時調用 onError 函數。

Web Worker 的最佳實踐

到此爲止咱們已經見識了 Web Worker 的強悍與不足,下面就一塊兒來看看最適合使用它的場景有哪些:

  • 光線追蹤(Ray Tracing)::光線追蹤屬於計算機圖形學中的 渲染(Rendering) 技術,它會追蹤並轉換光線 的軌跡爲一個個像素點,最終生成一張完整的圖片。爲模擬光線的軌跡,光線追蹤須要 CPU 進行大量的數學計算。光線追蹤包括模擬光的反射、折射及物質效果等。以上全部的計算邏輯均可以交給 Web Worker 完成,從而不阻塞 UI 線程的執行。或者更好的方案是使用多個 Worker (以及多個 CPU)來完成圖片渲染。這有一個使用 Web Worker 進行光線追蹤的 demo — nerget.com/rayjs-mt/ra….

  • 加密: 針對我的敏感數據的保護條例變得日益嚴格,端對端的數據加密也變得更爲流行。當程序中須要常常加密大量數據時(如向服務器發送數據),加密成爲了很是耗時的工做。Web Worker 能夠很是好的切入此類場景,由於這裏不涉及任何的 DOM 操做,Worker 中僅僅運行一些專爲加密的算法。Worker 會勤懇地默默工做,絲絕不會打擾用戶,也毫不會影響用戶的體驗。

  • 數據預獲取: 爲優化你的網站或 web 應用的數據加載時長,你可使用 Web Worker 預先獲取一些數據,存儲起來以備後續使用。Web Worker 在這裏發揮着重要做用,由於它毫不會影響應用的 UI 體驗,若不使用 Web Worker 狀況會變得異常糟糕。

  • Progressive Web App: 當網絡狀態不是很理想時,你仍需保證 PWA 有較快的加載速度。這就意味着 PWA 的數據須要被持久化到本地瀏覽器中。在此背景下,一些與 IndexDB 相似的 API 便應運而生了。從根本上來講,客戶端一側須要有數據存儲能力。爲保證存取時不阻塞 UI 線程,這部分工做理應交給 Web Worker 完成。好吧,在 IndexDB 中你能夠不使用 Web Worker,由於它提供的異步 API 一樣不會阻塞 UI。可是在這以前,IndexDB 提供的是同步API(可能會被再次引入),這種狀況使用 Web Worker 仍是很是有必要的。

  • 拼寫檢查: 進行拼寫檢查的基本流程以下 — 程序首先從詞典文件中讀取一系列拼寫正確的單詞。整個詞典的單詞會被解析爲一個搜索樹用於實際的文本搜索。當待測詞語被輸入後,程序會檢查已創建的搜索樹中是否存在該詞。若是在搜索樹中沒有匹配到待測詞語,程序會替換字符組成新的詞語,並測試新的詞語是不是用戶期待輸入的,若是是則會返回該詞語。整個檢測過程能夠被輕鬆 「下放」 給 Web Worker 完成,Worker 會完成全部的詞語檢索和詞語聯想工做,這樣一來用戶的輸入就不會阻塞 UI 了。

SessionStack 來講,保持高性能和高可靠性是極其重要的. 持有這種理念的主要緣由是,一旦你的應用集成 SessionStack 後,它會開始記錄從 DOM 變化、用戶交互行爲到網絡請求、未捕獲異常和 debug 信息的全部數據。收集到的跟蹤數據會被 實時 發送到後臺服務器,以視頻的形式向你還原應用中出現的問題,幫助你從用戶的角度重現錯誤現場。這一切功能的實現須要足夠的快而且不能給你的應用帶來任何性能上的負擔。

這就是爲何咱們儘量地把 SessionStack 中,值得優化的業務邏輯交給 Web Worker 完成。諸如在覈心監控庫和播放器中,都包含了像 hash 數據完整性驗證、渲染等 CPU 密集型任務,這些都是值得使用 Web Worker 優化的地方。

Web 技術持續向前變動和發展,因此咱們寧可先行一步也要保證 SessionStack 是一個不會給用戶 app 帶來任何性能損耗的輕量級應用。

若是閣下願意試試 SessionStack ,這裏有一個免費的試用計劃

參考資料


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索