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