閱讀目錄
一: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檢視(注意:這只是一個框架,內部沒有任何程式碼,我們可以把上面的程式碼複製到裡面去執行下即可)。