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


閱讀目錄

一: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查看(注意:這只是一個框架,內部沒有任何代碼,我們可以把上面的代碼復制到里面去運行下即可)。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM