Webpack 下使用 web workers 及 基本原理 和 應用場景

龍恩0707發表於2019-07-27

閱讀目錄

一:web workers的基本原理

我們都知道,我們的javascript採用的是單執行緒模型,所有的任務都在一個主執行緒中完成,一次只能執行一個任務,如果有多個任務需要被執行的話,那麼後面的任務會依次排隊等著,那麼這種情況下,如果我們需要處理大量的計算邏輯的時候,那麼就會比較耗時,那麼使用者介面就很有可能出現假死的狀態,或者瀏覽器被直接卡死,這樣非常影響使用者體驗。這個時候我們的web workers就出現了,來解決這樣類似的問題。

我們可以把javascrpt單執行緒模式理解我們日常生活中的快餐收銀員可能會更好的理解,我們平時吃快餐依次排隊,然後結賬,目前只有一個收銀員結賬,所有的人都要排隊依次來,那假如說某個人拿了很多很多菜,收營員需要慢慢的算賬到底要收多少錢,那麼這個時候一般會要多點時間,那麼其他人就在後面排著隊等著,等前面的結賬完成後,再依次去結賬,這樣就會很耗時,那假如現在收銀員有2個或更多的地方結賬的話,我們就可以到其他人少的地方去結賬了,這樣就可以使速度更快的去完成某個任務,其實現在我們的 web workers 也是這麼一種機制,一些複雜業務邏輯,我們的主執行緒可以把這些任務交給 web workers子執行緒去處理,子執行緒處理完成後,會把結果返回給主執行緒,然後我們的主執行緒就執行。

web workers的作用:它使用javascript建立workers執行緒,我們瀏覽器主執行緒可以把一些複雜的業務處理邏輯交給worker執行緒去執行,在我們的主執行緒執行的同時,我們的worker執行緒也在後臺執行,兩者互補干擾。等到worker執行緒完成計算任務的時候,會再把結果返回給主執行緒。這樣的優點是:一些複雜的計算邏輯,我們可以把它交給worker執行緒去完成,主執行緒就會很流暢,不會被阻塞了。

Worker執行緒一旦建立成功了,就會始終執行了,不會被主執行緒的執行打斷,雖然這樣有利於隨時響應主執行緒的通訊,但是這也造成了Worker比較耗費資源,在我們編碼過程中,可以適當的使用,並且如果使用完成後,我們應該需要關閉。

Web Worker 使用注意如下常見的幾點:

1. 同源限制:分配給 Worker執行緒執行的指令碼檔案,必須與主執行緒的指令碼檔案同源。
2. DOM限制:Worker所在的執行緒它不能操作window,document這樣的物件,也就是說worker執行緒不能操作dom物件
,但是worker執行緒可以操作業務邏輯,然後把操作後的結果返回給主執行緒,主執行緒再去做相關的DOM操作。

3. 檔案限制:Worker執行緒無法讀取本地檔案,也就是說不能開啟本機的檔案系統(如:file://) 這樣的,它所載入的指令碼,必須來自網路。

Web Worker 瀏覽器支援程度如下所示:

二:web Workers 的基本用法

1. 建立worker執行緒方法:

我們在主執行緒js中呼叫 new 命令,然後實列化 Worker()建構函式,就可以建立一個Worker執行緒了,如下程式碼所示:

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

Worker()建構函式的引數是一個指令碼檔案,該檔案就是Worker執行緒需要執行的任務,由於Worker不能讀取本地檔案,所以這個指令碼必須來自網路。

如果我們在本地呼叫 work.js 執行緒的話,就會報如下錯

如上初始化完成後,我們的主執行緒需要向子執行緒傳送訊息,使用 worker.postMessage()方法,向Worker傳送訊息。如下所示:

worker.postMessage('hello world');

worker.postMessage 方法可以接受任何型別的引數,甚至包括二進位制資料。

傳送訊息完成後,子執行緒去處理操作,然後把結果返回給主執行緒,那麼主執行緒通過 worker.onmessage 指定監聽函式,接收子執行緒傳送回來的訊息,如下程式碼所示:

worker.onmessage = function(event) {
  console.log('接收到的訊息為: ' + event.data);
}

如上程式碼,事件物件的data屬性可以獲取worker傳送回來的訊息。

如果我們的worker執行緒任務完成後,我們的主執行緒可以把它關閉掉,使用如下程式碼:

worker.terminate();

2. worker執行緒

Worker執行緒內部需要有一個監聽函式,監聽主執行緒/其他子執行緒 傳送過來的訊息。監聽事件為 'message'. 如下程式碼所示:

self.addEventListener('message', function(e) {
  self.postMessage('子執行緒向主執行緒傳送訊息: ' + e.data);
  self.close(); // 關閉自身
});

如上程式碼,self代表子執行緒本身,也可以為子執行緒的全域性物件。

注意:主執行緒向子執行緒傳送訊息為:worker.postMessage('hello world'); 這樣的,但是子執行緒向主執行緒傳送訊息,是如下程式碼所示:

self.postMessage('子執行緒向主執行緒傳送訊息: ' + e.data);

其實上面的寫法 和如下兩種寫法是等價的,如下程式碼:

// 寫法一
this.addEventListener('message', function(e) {
  this.postMessage('子執行緒向主執行緒傳送訊息: ' + e.data);
  this.close(); // 關閉自身
});

// 寫法二
addEventListener('message', function(e) {
  postMessage('子執行緒向主執行緒傳送訊息: ' + e.data);
  close(); // 關閉自身
});

注意:如果我們使用了 self.addEventListener 來監聽函式的話,那麼我們也要使用 self.postMessage() 這樣的來傳送訊息,如果我們使用 this.addEventListener 來監聽函式的話,那麼也應該使用 this.postMessage 來傳送訊息,如果我們使用如下方法: addEventListener('message', function(e) {}); 來監聽函式的話,那麼我們就可以使用 postMessage()方法來傳送訊息的。

3. 瞭解 importScripts() 方法

如果我們的worker內部需要載入其他的指令碼的話,我們可以使用 importScripts() 方法。如下程式碼所示:

importScripts('a.js');

當然該方法也可以載入多個指令碼,如下程式碼所示:

importScripts('a.js', 'b.js');

4. 錯誤處理

主執行緒可以監聽Worker執行緒是否發生錯誤,如果發生錯誤,Worker執行緒會觸發主執行緒的error事件。

// 方法一
worker.onerror(function(e) {
  console.log(e);
});

// 方法二

worker.addEventListener('error', function(e) {
  console.log(e);
});

worker執行緒內部也是可以監聽 error 事件的。

5. 關閉執行緒

如果我們的執行緒使用完畢後,為了節省系統資源,我們需要關閉執行緒。如下方法:

// 關閉主執行緒
worker.terminate();

// 關閉子執行緒
self.close();

三:在webpack中配置 Web Workers

 還是和之前一樣,配置之前,我們來看下我們專案整個目錄架構如下:

|--- web-worker專案
| |--- node_modules          # 安裝的依賴包
| |--- public
| | |--- images              # 存放專案中所有的圖片
| | |--- js
| | | |--- main.js           # js 的入口檔案
| | | |--- test1.worker.js   # worker 執行緒的js檔案
| | |--- styles              # 存放所有的css樣式
| | |--- index.html          # html檔案
| |--- package.json
| |--- webpack.config.js

1. 在專案中安裝 worker-loader 依賴,如下命令所示:

npm install -D worker-loader

2. 在webpack配置中新增如下配置:

module.exports = {
  module: {
    rules: [
      {
        test: /\.worker\.js$/, // 以 .worker.js 結尾的檔案將被 worker-loader 載入
        use: { 
          loader: 'worker-loader',
          options: {
            inline: true
            // fallback: false
          }
        }
      }
    ]
  }
}

如上正則匹配的是以 以 .worker.js 結尾的檔案將被 worker-loader 載入, 也就是說我們在專案中我們的worker檔名可以叫 test.worker.js 類似這樣的名字,或其他的,只要保證 xxx.worker.js 這樣的檔名即可。

在上面配置中,設定 inline 屬性為 true 將 worker 作為 blob 進行內聯;內聯模式將額外為瀏覽器建立 chunk,即使對於不支援內聯 worker 的瀏覽器也是這樣的;比如如下執行,我們可以在我們的本地專案下看到有如下這麼一個請求:

在開發環境下或正式環境中 我們要如何配置呢?

如果在本地開發中,我們會使用 webpack-dev-server 啟動本地調式伺服器,如果只有上面的配置的話,我們可以在控制檯中會報如下的錯誤;"Uncaught ReferenceError: window is not defined"; 這樣的錯誤,如下所示:

要解決如上的錯誤的話,我們需要在我們的webpack配置檔案下的out下,加一個屬性 globalObject: 'this'; 如下程式碼:

module.exports = {
  output: {
    globalObject: 'this'
  }
}

比如我現在的webpack配置如下:

module.exports = {
  output: {
    filename: process.env.NODE_ENV === 'production' ? '[name].[contenthash].js' : '[name].js',
    // 將輸出的檔案都放在dist目錄下
    path: path.resolve(__dirname, 'dist'),
    publicPath: '/',
    globalObject: 'this'
  }
}

然後我們繼續執行下 就沒有報錯了。

首先來看下我們的 public/js/main.js 程式碼如下:

// 載入css樣式
require('../styles/main.styl');

import Worker from './test1.worker.js';

// 建立worker實列
var worker = new Worker();

// 向worker執行緒傳送訊息
worker.postMessage('主執行緒向worker執行緒傳送訊息');

// 監聽worker執行緒傳送回來的訊息

worker.onmessage = function(e) {
  console.log('監聽worker執行緒傳送回來的訊息如下所示:')
  console.log(e);
};

然後我們的 public/js/test1.worker.js(子執行緒)的程式碼如下所示:

// 監聽訊息
onmessage = function(e) {
  console.log('監聽到的訊息為:' + e.data);
}

const msg = '工作執行緒向主執行緒傳送訊息';

// 傳送訊息
postMessage(msg);

然後執行結果如下所示:

如上程式碼,我們首先建立了一個worker實列,如程式碼:var worker = new Worker(); 然後他就會呼叫 test1.worker.js 程式碼,該worker中的程式碼會首先給主執行緒傳送訊息,訊息文字為: '工作執行緒向主執行緒傳送訊息'; 然後我們的主執行緒中會通過 worker.onmessage 事件來監聽子執行緒的訊息,因此我們第一次列印出來為如下程式碼的訊息:

worker.onmessage = function(e) {
  console.log('監聽worker執行緒傳送回來的訊息如下所示:')
  console.log(e);
};

然後我們的主執行緒才會向子執行緒傳送訊息,如下程式碼:

// 向worker執行緒傳送訊息
worker.postMessage('主執行緒向worker執行緒傳送訊息');

然後 test1.worker.js 程式碼中的 onmessage 就能監聽到訊息,如下所示:

// 監聽訊息
onmessage = function(e) {
    console.log('監聽到的訊息為:' + e.data);
}

最後就會列印出資訊如下:"監聽到的訊息為:主執行緒向worker執行緒傳送訊息"。

四:Web Worker的應用場景

4.1. 使用 web workers 來解決耗時較長的問題

現在我們需要做一個這樣的demo,我們在頁面中有一個input輸入框,使用者需要在該輸入框中輸入數字,然後點選旁邊的計算按鈕,在後臺計算從1到給定數值的總和。如果我們不使用web workers 來解決該問題的話,如下demo程式碼所示:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>web worker 實列</title>
</head>
<body>
  <h1>從1到給定數值的求和</h1>
  輸入數值: <input type="text" id="num" />
  <button onclick="calculate()">計算</button>

  <script type="text/javascript">
    function calculate() {
      var num = parseInt(document.getElementById("num").value, 10);
      var result = 0;
      // 迴圈計算求和
      for (var i = 0; i <= num; i++) {
        result += i;
      }
      alert('總和魏:' + result + '');
    }
  </script>
</body>
</html>

如上程式碼,然後我們輸入 1百億,然後讓計算機去幫我們計算,計算的時間應該要20秒左右的時間,但是在這20秒之前的時間,那麼我們的頁面就處於卡頓的狀態,也就是說什麼都不能做,等計算結果出來後,我們就會看到如下彈窗提示結果了,如下所示:

那現在我們需要使用我們的 web workers 來解決該問題,我們希望把這些耗時操作使用 workers去解決,那麼主執行緒就不影響頁面假死的狀態了,我們首先把index.html 程式碼改成如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>web worker 實列</title>
</head>
<body>
  <h1>從1到給定數值的求和</h1>
  輸入數值: <input type="text" id="num" />
  <button id="calculate">計算</button>
</body>
</html>

然後在我們的 public/js/main.js 程式碼如下:

// 載入css樣式
require('../styles/main.styl');

import Worker from './test1.worker.js';

// 建立worker實列
var worker = new Worker();

var calDOM = document.getElementById('calculate');
calDOM.addEventListener('click', calculate);

function calculate() {
  var num = parseInt(document.getElementById("num").value, 10);
  // 將我們的資料傳遞給 worker執行緒,讓我們的worker執行緒去幫我們做這件事
  worker.postMessage(num);
}

// 監聽worker執行緒的結果
worker.onmessage = function(e) {
  alert('總和值為:' + e.data);
};

public/js/test1.worker.js 程式碼如下:

// 監聽訊息
onmessage = function(e) {
  var num = e.data;
  var result = 0;
  for (var i = 0; i <= num; i++) {
    result += i;
  } 
  // 把結果傳送給主執行緒
  postMessage(result);
}

如上程式碼我們執行下可以看到,我們點選下計算按鈕後,我們使用主執行緒把該複雜的耗時操作給子執行緒處理後,我們點選按鈕後,我們的頁面就可以操作了,因為主執行緒和worker執行緒是兩個不同的環境,worker執行緒的不會影響主執行緒的。因此如果我們需要處理一些耗時操作的話,我們可以使用 web workers執行緒去處理該問題。

4.2. 實現建立內嵌的worker

如上是在webpack中配置使用 web workers 的使用,我們也可以實現建立內嵌的worker,那麼什麼是 內嵌的worker呢?首先我們把 webpack 中的如下配置程式碼註釋掉:

module.exports = {
  output: {
    // globalObject: 'this'
  }
}

然後我們執行程式碼,肯定報錯:'Uncaught ReferenceError: window is not defined'。 那麼現在我們使用 建立內嵌的worker來解決這樣的問題。我們通過 URL.createObjectURL()建立URL物件,可以實現建立內嵌的worker。我們把上面的 test1.worker.js 程式碼寫到一個js檔案裡面,也就是寫到main.js裡面去,如下程式碼:

var myTask = `onmessage = function(e) {
  var num = e.data;
  var result = 0;
  for (var i = 0; i <= num; i++) {
    result += i;
  } 
  // 把結果傳送給主執行緒
  postMessage(result);
}`;

var blob = new Blob([myTask]);
var myWorker = new Worker(window.URL.createObjectURL(blob));

// 建立worker實列
// var worker = new Worker();

var calDOM = document.getElementById('calculate');
calDOM.addEventListener('click', calculate);

function calculate() {
  var num = parseInt(document.getElementById("num").value, 10);
  // 將我們的資料傳遞給 worker執行緒,讓我們的worker執行緒去幫我們做這件事
  myWorker.postMessage(num);
}

// 監聽worker執行緒的結果
myWorker.onmessage = function(e) {
  alert('總和值為:' + e.data);
};

注意:這邊只是簡單的演示下 web worker 能解決一些耗時操作的問題,如果想要學習更多關於web workers 可以自己google下折騰下。我這邊先到此了。也就是說,如果在一些js耗時的程式碼,我們可以使用子執行緒來解決類似的問題,這樣就不會導致頁面被卡死的狀態。
web-worker 專案github檢視(注意:這只是一個框架,內部沒有任何程式碼,我們可以把上面的程式碼複製到裡面去執行下即可)。

相關文章