JavaScript 工做原理之七-Web Workers 分類及 5 個使用場景

Web Workers 分類及 5 個使用場景

原文請查閱 這裏,略有刪減,本文采用 知識共享署名 4.0 國際許可協議共享,BY Troland

這是 JavaScript 工做原理的第七章。javascript

本系列持續更新中,Github 地址請查閱這裏html

如今,咱們將會剖析 Web Workers:咱們將會綜合比較不一樣類型的 workers,如何組合運用他們的構建模塊來進行開發以及不一樣場景下各自的優缺點。最後,咱們將會介紹 5 個 Web Workder 的使用場景。html5

前面的詳細介紹的文章中你已經清楚地瞭解到 JavaScript 是單線程的事實。然而,JavaScript 也容許開發者編寫異步代碼。java

異步編程的侷限性

前面咱們瞭解到異步編程及其使用時機。node

異步編程經過調度部分代碼使之在事件循環中延遲執稈,這樣就容許優先渲染程序界面,從而讓程序運行流暢。git

AJAX 請求是一個很好的異步編程的使用場景 。由於請求可能會花很長的時間,因此能夠異步執行它們,而後在客戶端等待數據返回的同時,運行其它代碼。github

// 假設使用 jQuery
jQuery.ajax({
    url: 'https://api.example.com/endpoint',
    success: function(response) {
            // 當數據返回時候的代碼
    }
});

然而,這裏會產生一個問題-AJAX 請求是由瀏覽器網頁 API 進行處理的,能夠異步執行其它代碼嗎?好比,假設成功回調的代碼是 CPU 密集型的:web

var result = performCPUIntensiveCalculation();

若是 performCPUIntensiveCalculation 不是一個 HTTP 請求而是一個會阻塞界面渲染的代碼(好比大量的 for 循環),這樣就沒有辦法釋放事件循環和瀏覽器的 UI-瀏覽器會被凍結住且失去響應。ajax

這意味着,異步函數只是是解決了一部分 JavaScript 的單線程限制。算法

在某些狀況下,你能夠經過使用 setTimeout 來很好地解決因爲長時間計算所形成的 UI 阻塞。好比,經過把一個複雜的計算批量拆分爲若干個setTimeout 調用 ,把它們放在事件循環的不一樣位置執行,而後這樣就可使得 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) {
            // 在事件循環中調用下一個函數
            setTimeout(function() {
                sum += numbers[i];
                calculateSumAsync(i + 1);
            }, 0);
        } else {
             // 到達數組末尾,調用回調
            callback(sum / len);
        }
    }

    calculateSumAsync(0);
}

這裏利用 setTimeout 函數在事件循環中循序添加每一次計算。在每一次計算之間,將會有充足的時間來進行其它的計算和解凍瀏覽器。

Web Workders 來救場

HTML5 給咱們帶了不少開箱即用的好用的功能,包括:

  • SSE(以前文章中提到過而且和 WebSockets 進行了比較)
  • Geolocation
  • Application cache
  • Local Storage
  • Drag and Drop
  • Web Workers

Web Workers 是瀏覽器內置的線程因此能夠被用來執行非阻塞事件循環的 JavaScript 代碼。

屌爆了。整個 JavaScript 是基於單線程環境的而 Web Workers (部分)能夠突破這方面的限制。

Web Workers 容許開發者把長時間運行和密集計算型的任務放在後臺執行而不會阻塞 UI,這會使得應用程序運行得更加流暢。另外,這樣就不用再使用 setTimeout 的黑科技來防止阻塞事件循環了。

這裏有一個展現使用和未使用 Web Workers 來進行數組排序的區別的示例

Web Workers 概覽

Web Workers 容許你作諸如運行處理 CPU 計算密集型任務的耗時腳本而不會阻塞 UI 的事情。事實上,全部這些操做都是並行執行的。Web Workers 是真正的多線程。

你或許會有疑問-『難道 JavaScript 不是單線程的嗎?』。

當你意識到 JavaScript 是一門沒有定義線程模型的語言的時候,或許你會感受很是的驚訝。Web Workers 並非 JavaScript 的一部分,他們是能夠經過 JavaScript 進行操做的瀏覽器功能之一。之前,大多數的瀏覽器是單線程的(固然,如今已經變了),並且大多數的 JavaScript 功能是在瀏覽器端實現完成的。Node.js 沒有實現 Web Workers -它有 『cluster』和 『child_process』的概念,這二者和 Web Workers 有些許差別。

值得注意的是,規範中有三種類型的 Web Workers:

Dedicated Workers

Dedicated Web Workers 是由主進程實例化而且只能與之進行通訊

<center>Dedicated Workers 瀏覽器支持狀況</center>

Shared Workers

Shared workers 能夠被運行在同源的全部進程訪問(不一樣的瀏覽的選項卡,內聯框架及其它shared workers)。

<center>Shared Workers 瀏覽器支持狀況</center>

Service Workers

Service Worker 是一個由事件驅動的 worker,它由源和路徑組成。它能夠控制它關聯的網頁,解釋且修改導航,資源的請求,以及一種很是細粒度的方式來緩存資源以讓你很是靈活地控制程序在某些狀況下的行爲(好比網絡不可用)。

<center>Service Workers 瀏覽器支持狀況</center>

本篇文章,咱們將會專一於 Dedicated Workers 並以 『Web Workers』或者 『Workers』來稱呼它。

Web Workers 運行原理

Web Workers 是以加載 .js 文件的方式實現的,這些文件會在頁面中異步加載。這些請求會被 Web Worker API 徹底隱藏。

Workers 使用類線程的消息傳輸-獲取模式。它們很是適合於爲用戶提供最新的 UI ,高性能及流暢的體驗。

Web Workers 運行於瀏覽器的一個隔離線程之中。所以,他們所執行的代碼必須被包含在一個單獨的文件之中。請謹記這一特性。

讓咱們看如何建立初始化 worker 吧:

var worker = new Worker('task.js');

若是 『task.js』文件存在且可訪問,瀏覽器會生成一個線程來異步下載文件。當下載完成的時候,文件會當即執行而後 worker 開始運行。萬一文件不存在,worker 會運行失敗且沒有任何提示。

爲了啓動建立的 worker,你須要調用 postMessage 方法:

worker.postMessage();

Web Worker 通訊

爲了在 Web Worker 和 建立它的頁面間進行通訊,你得使用 postMessage 方法或者一個廣播信道

postMessage 方法

最新的瀏覽器支持方法的第一參數爲一個 JSON 對象而舊的瀏覽器只支持字符串。

讓咱們來看一個例子,經過往 worker 的方法的第一個參數傳入更爲複雜的 JSON 對象來理解其建立者頁面是如何與之進行來回通訊的。傳入字符串與之相似。

讓咱們看下如下的 HTML 頁面(或者更準確地說是 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);

當點擊按鈕,會在主頁面調用 postMessage 方法。

worker.postMessage 行代碼會把包含 cmddata 屬性及其各自屬性值的 JSON 對象傳入 worker。worker 經過定義監聽 message 事件來處理傳過來的消息。

當接收到消息的時候,worker 會執行實際的計算而不會阻塞事件循環。worker 會檢查傳進來的 e 事件,而後像一個標準的 JavaScript 函數那樣運行。當運行結束,傳回主頁面計算結果。

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

有兩種方法來中斷 woker 的執行:主頁面中調用 worker.terminate() 或者在 workder 內部調用 self.close()

廣播信道

Broadcast Channel 是更爲廣泛的通訊接口。它容許咱們向共享同一個源的全部上下文發送消息。同一個源下的全部的瀏覽器選項卡,內聯框架或者 workers 均可以發送和接收消息:

// 鏈接到一個廣播信道
var bc = new BroadcastChannel('test_channel');

// 發送簡單信息示例
bc.postMessage('This is a test message.');

// 一個在控制檯打印消息的簡單事件處理程序示例
// logs the message to the console
bc.onmessage = function (e) { 
  console.log(e.data); 
}

// 關閉信道
bc.close()

視覺上看,你能夠經過廣播信道的圖例以更加深入的理解它。

<center>全部的瀏覽器上下文都是同源的</center>

然而,廣播信道瀏覽器兼容性不太好:

消息大小

有兩種向 Web Workers 發送消息的方法:

  • 複製消息:消息被序列化,複製,而後發送出去,接着在接收端反序列化。頁面和 worker 沒有共享一個相同的消息實例,因此在每次傳遞消息過程當中最後的結果都是複製的。大多數瀏覽器是經過在任何一端自動進行 JSON 編碼/解碼消息值來實現這一功能。正如所預料的那樣,這些對於數據的操做顯著增長了消息傳送的性能開銷。消息越大,傳送的時間越長。
  • 消息傳輸:這意味着最初的消息發送者一發送即再也不使用(<!--和導彈的發射後無論同樣-->)。數據傳輸很是的快。惟一的限制即只能傳輸 ArrayBuffer 數據對象。

Web Workers 的可用功能

因爲 Web Workers 的多線程特性,它只能使用一部分 JavaScript 功能。如下是可以使用的功能列表:

  • navigator 對象
  • location 對象(只讀)
  • XMLHttpRequest
  • setTimeout()/clearTimeout()setInterval()/clearInterval()
  • Application Cache
  • 使用 importScripts 來引用外部腳本
  • 建立其它 web workers

Web Worker 的侷限性

使人沮喪的是,Web Workers 不可以訪問一些很是關鍵的 JavaScript 功能:

  • DOM(非線程安全的)
  • window 對象
  • document 對象
  • parent 對象

這意味着 Web Worker 不可以操做 DOM(所以不能更新 UI)。有時候,這會讓人很蛋疼,不過一旦你學會如何合理地使 Web Workers,你就會把它當成單獨的『計算機器』來使用而用其它頁面代碼來操做 UI。Workers 將會爲你完成繁重的計算任務而後一旦任務完成,會把結果傳到頁面中並對界面進行必要的更新。

錯誤處理

和任何 JavaScript 代碼同樣,你會想要處理 Web Workers 中的任何錯誤。當在 worker 執行過程當中有錯誤發生的時候,會觸發 ErrorEvent 事件。這個接口包含三個有用的屬性來指出錯誤的地方:

  • filename-引發錯誤的 worker 腳本名稱
  • 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 事件。

在 worker 中(在 workerWithError 中),咱們經過未在做用域中定義的 x 乘以 2 來建立一個意圖錯誤。異常會傳播到初始化腳本(即主頁面中)而後調用 onError 並傳入關於錯誤的信息。

Web Workers 最佳使用場景

迄今爲止,咱們列舉了 Web Workers 的長處及其限制。讓咱們看看他們的最佳使用場景:

  • 射線追蹤:射線追蹤是一項經過追蹤光線的路徑做爲像素來生成圖片的渲染技術。Ray tracing 使用 CPU 密集型計算來模仿光線的路徑。思路即模仿一些諸如反射,折射,材料等的效果。全部的這些計算邏輯能夠放在 Web Worker 中以免阻塞 UI 線程。甚至更好的方法即-你能夠輕易地把把圖片的渲染拆分在幾個 workers 中進行(即在各自的 CPU 中進行計算,意思是說利用多個 CPU 來進行計算,能夠參考下 nodejs 的 api)。這裏有一個使用 Web Workers 來進行射線追蹤的簡單示例-https://nerget.com/rayjs-mt/r...
  • 加密:端到端的加密因爲對保護我的和敏感數據日益嚴格的法律規定而變得愈來愈流行。加密有時候會很是地耗時,特別是若是當你須要常常加密不少數據的時候(好比,發往服務器前加密數據)。這是一個使用 Web Worker 的絕佳場景,由於它並不須要訪問 DOM 或者利用其它魔法-它只是純粹使用算法進行計算而已。一旦在 worker 進行計算,它對於用戶來講是無縫地且不會影響到用戶體驗。
  • 預取數據:爲了優化網站或者網絡應用及提高數據加載時間,你可使用 Workers 來提早加載部分數據以備不時之需。不像其它技術,Web Workers 在這種狀況下是最棒噠,由於它不會影響程序的使用體驗。
  • 漸進式網絡應用:即便在網絡不穩定的狀況下,它們必須快速加載。這意味着數據必須本地存儲於瀏覽器中。這時候 IndexDB 及其它相似的 API 就派上用場了。大致上說,一個客戶端存儲是必須的。爲了避免阻塞 UI 線程的渲染,這項工做必須由 Web Workers 來執行。呃,當使用 IndexDB的時候,能夠不使用 workers 而使用其異步接口,可是以前它也含有同步接口(可能會再次引入 ),這時候就必須在 workers 中使用 IndexDB。

    這裏須要注意的是在現代瀏覽器已經不支持同步接口了,具體可查看這裏

  • 拼寫檢查:一個基本的拼寫檢測器是這樣工做的-程序會讀取一個包含拼寫正確的單詞列表的字典文件。字典會被解析成一個搜索樹以加快實際的文本搜索。當檢查器檢查一個單詞的時候,程序會在預構建搜索樹中進行檢索。若是在樹中沒有檢索到,則會經過提供替代的字符爲用戶提供替代的拼寫並檢測單詞是不是有效-是不是用戶須要的單詞。這個檢索過程當中的全部工做均可以交由 Web Worker 來完成,這樣用戶就只需輸入單詞和語句而不會阻塞 UI,與此同時 worker 會處理全部的搜索和服務建議。

SessionStack 中對於咱們來講性能和可靠性是相當重要的。之因此這麼重要的緣由是一旦把 SessionStack 整合進網絡應用,它就會開始收集從 DOM 變化,用戶交互到網絡請求,未處理異常和調試信息的全部一切信息。全部的數據都是即時傳輸到咱們的服務器的,這樣就容許你以視頻的方式重放網絡應用中的全部問題以及觀察用戶端產生的一切問題。全部的一切都只會給你的程序帶來極小的延遲且沒有任何的性能開銷。

這就是爲何咱們使用 Web Workers 來處理監視庫和播放器的邏輯的緣由,由於 Web Workers 會幫咱們處理諸如使用哈希來驗證數據完整性,渲染等 CPU 密集型的任務。

在這個網絡技術突飛猛進的時代,咱們更加努力地保證 SessionStack 輕巧且不會給用戶程序帶來任何性能影響。

擴展

實際工做過程會遇到用戶須要經過解析遠程圖片來得到圖片 base64 的案例,那麼這時候,若是圖片很是大,就會形成 canvas 的 toDataURL 操做至關的耗時,從而阻塞頁面的渲染。

因此解決思路即把這裏的處理圖片的操做交由 worker 來處理。如下貼出主要的代碼:

<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <meta charset="UTF-8">
  <title>Canvas to base64</title>
</head>
<body>
  <script>
    function loadImageAsync(url) {
      if (typeof url !== 'string') {
        return Promise.reject(new TypeError('must specify a string'));
      }

      return new Promise(function(resolve, reject) {
        const image = new Image();
        // 容許 canvas 跨域加載圖片
        image.crossOrigin="anonymous";
        image.onload = function() {
          const $canvas = document.createElement('canvas');
          const ctx = $canvas.getContext('2d');
          const width = this.width;
          const height = this.height;
          let imageData;
          
          $canvas.width = width;
          $canvas.height = height;
          ctx.drawImage(image, 0, 0, width, height);
          imageData = ctx.getImageData(0, 0, $canvas.width, $canvas.height);
          resolve({image, imageData});
        };

        image.onerror = function() {
          reject(new Error('Could not load image at ' + url));
        };

        image.src = url;
      });
    }
    
    function blobToDataURL(blob) {
      return new Promise((fulfill, reject) => {
        let reader = new FileReader();
        reader.onerror = reject;
        reader.onload = (e) => fulfill(reader.result);
        reader.readAsDataURL(blob);
      })
    }

    document.addEventListener("DOMContentLoaded", function () {
      loadImageAsync('https://cdn-images-1.medium.com/max/1600/1*4lHHyfEhVB0LnQ3HlhSs8g.png')
        .then(function (image) {
          // jpeg-web-worker.js https://github.com/kentmw/jpeg-web-worker
          const worker = new Worker('jpeg-web-worker.js');
          worker.postMessage({
            image: image.imageData,
            quality: 50
          });
          worker.onmessage = function(e) {
            // e.data is the imageData of the jpeg. {data: U8IntArray, height: int, width: int}
            // you can still convert the jpeg imageData into a blog like this:
            const blob = new Blob( [e.data.data], {type: 'image/png'} );
            blobToDataURL(blob).then((imageURL) => {
              console.log('imageUrl:', imageURL);
            })
          }
        })
        .catch(function (err) {
          console.log('Error:', err.message);
        });
    });
  </script>
</body>
</html>

以上是經過 canvas 來獲取圖片數據,那麼是否有其它方法呢?確定有的啦,動下腦筋吧少年。

本系列持續更新中,Github 地址請查閱這裏

相關文章
相關標籤/搜索