前端er來學習一下webWorker吧

OBKoro1發表於2018-11-27

webWorker 是什麼[webWorker 快速上手]

我們都知道,JavaScript 是單執行緒的,在同一時刻只能處理一個任務,我們會通過 setTimeout()、setInterval()、ajax 和事件處理程式等技術模擬“並行”。但都不是真正意義上的並行:

Web Worker 是 HTML5 標準的一部分,這一規範定義了一套 API,它允許一段 JavaScript 程式執行在主執行緒之外的另外一個執行緒中。

這在很大程度上利用了現在不斷升級的電腦計算能力:能夠在同一時間平行處理兩個任務。

游泳、健身瞭解一下:部落格前端積累文件公眾號GitHub

場景

當我們有些任務需要花費大量的時間,進行復雜的運算,就會導致頁面卡死:使用者點選頁面需要很長的時間才能響應,因為前面的任務還未完成,後面的任務只能排隊等待。對使用者來說,這樣的體驗無疑是糟糕的,web worker 就是為了解決這種花費大量時間的複雜運算而誕生的!

WebWorker 的作用:建立 worker 執行緒

WebWorker 允許在主執行緒之外再建立一個 worker 執行緒,在主執行緒執行任務的同時,worker 執行緒也可以在後臺執行它自己的任務,互不干擾

這樣就讓 JS 變成多執行緒的環境了,我們可以把高延遲、花費大量時間的運算,分給 worker 執行緒,最後再把結果返回給主執行緒就可以了,因為時間花費多的任務被 web worker 承擔了,主執行緒就會很流暢了!


主執行緒

我們先來看一下栗子:

codepen,這裡我寫了一個 class,裡面有詳細註釋,可以參考一下。

建立 worker 物件:

主執行緒呼叫new Worker()建構函式,新建一個 worker 執行緒,建構函式的引數是一個 url,生成這個 url 的方法有兩種:

  1. 指令碼檔案:

    const worker = new Worker('https://~.js');
    複製程式碼

    因為 worker 的兩個限制:

    1. 分配給 Worker 執行緒執行的指令碼檔案,必須與主執行緒的指令碼檔案同源

    2. worker 不能讀取本地的檔案(不能開啟本機的檔案系統file://),它所載入的指令碼必須來自網路。

    可以看到限制還是比較多的,如果要使用這種形式的話,在專案中推薦把檔案放在靜態資料夾中,打包的時候直接拷貝進去,這樣我們就可以拿到固定的連結了,

  2. 字串形式:

    const data = `
        //  worker執行緒 do something
        `;
    // 轉成二進位制物件
    const blob = new Blob([data]);
    // 生成url
    const url = window.URL.createObjectURL(blob);
    // 載入url
    const worker = new Worker(url);
    複製程式碼

    栗子中就是使用這種形式的,方便我們演示。

    在專案中:我們可以把worker執行緒的邏輯寫在js檔案裡面,然後字串化,然後再export、import,配合webpack進行模組化管理,這樣就很容易使用了。

主執行緒的其他 API:

1. 主執行緒與 worker 執行緒通訊:

worker.postMessage({
  hello: ['hello', 'world']
});
複製程式碼

它們相互之間的通訊可以傳遞物件和陣列,這樣我們就可以根據相互之間傳遞的資訊來進行一些操作,比如可以設定一個type屬性,當值為hello時執行什麼函式,當值為world的時候執行什麼函式。

值得注意的是:它們之間通訊是通過拷貝的形式來傳遞資料的,進行傳遞的物件需要經過序列化,接下來在另一端還需要反序列化。這就意味著:

  1. 我們不能傳遞不能被序列化的資料,比如函式,會丟擲錯誤的。
  2. 在一端改變資料,另外一端不會受影響,因為資料不存在引用,是拷貝過來的。

2. 監聽 worker 執行緒返回的資訊

worker.onmessage = function (e) {
    console.log('父程式接收的資料:', e.data);
    // doSomething();
}
複製程式碼

3. 主執行緒關閉 worker 執行緒

Worker 執行緒一旦新建成功,就會始終執行,這樣有利於隨時響應主執行緒的通訊。

這也是 Worker 比較耗費計算機的計算資源(CPU)的原因,一旦使用完畢,就應該關閉 worker 執行緒。

worker.terminate(); // 主執行緒關閉worker執行緒
複製程式碼

4. 監聽錯誤

// worker執行緒報錯
worker.onerror = e => {
    // e.filename - 發生錯誤的指令碼檔名;e.lineno - 出現錯誤的行號;以及 e.message - 可讀性良好的錯誤訊息
    console.log('onerror', e);
};
複製程式碼

也可以像我給出的栗子一樣,把兩個報錯放在一起寫,有報錯把資訊傳出來就好了。


Worker 執行緒

self 代表 worker 程式自身

worker 執行緒的執行上下文是一個叫做WorkerGlobalScope的東西跟主執行緒的上下文(window)不一樣。

我們可以使用self/WorkerGlobalScope來訪問全域性物件。

監聽主執行緒傳過來的資訊:

self.onmessage = e => {
    console.log('主執行緒傳來的資訊:', e.data);
    // do something
};
複製程式碼

傳送資訊給主執行緒

self.postMessage({
    hello: [ '這條資訊', '來自worker執行緒' ]
});
複製程式碼

worker 執行緒關閉自身

self.close()
複製程式碼

worker 執行緒載入指令碼:

Worker 執行緒能夠訪問一個全域性函式 imprtScripts()來引入指令碼,該函式接受 0 個或者多個 URI 作為引數。

importScripts('http~.js','http~2.js');
複製程式碼
  1. 指令碼中的全域性變數都能被 worker 執行緒使用。

  2. 指令碼的下載順序是不固定的,但執行時會按照傳入 importScripts() 中的檔名順序進行,這個過程是同步的。

Worker 執行緒限制

因為 worker 創造了另外一個執行緒,不在主執行緒上,相應的會有一些限制,我們無法使用下列物件:

  1. window 物件
  2. document 物件
  3. DOM 物件
  4. parent 物件

我們可以使用下列物件/功能

  1. 瀏覽器:navigator 物件

  2. URL:location 物件,只讀

  3. 傳送請求:XMLHttpRequest 物件

  4. 定時器:setTimeout/setInterval,在 worker 執行緒輪詢也是很棒!

  5. 應用快取:Application Cache


多個 worker 執行緒

  1. 在主執行緒內可以建立多個 worker 執行緒

    栗子最下方有。

  2. worker 執行緒內還可以新建 worker 執行緒,使用同源的指令碼檔案建立

    在 worker 執行緒內再新建 worker 執行緒就不能使用window.URL.createObjectURL(blob),需要使用同源的指令碼檔案來建立新的 worker 執行緒,因為我們無法訪問到window物件。

    這裡不方便演示,跟在主執行緒建立 worker 執行緒是一個套路,只是改成了指令碼檔案形式建立 worker 執行緒。

執行緒間轉移二進位制資料

因為主執行緒與 worker 執行緒之間的通訊是拷貝關係,當我們要傳遞一個巨大的二進位制檔案給 worker 執行緒處理時(worker 執行緒就是用來幹這個的),這時候使用拷貝的方式來傳遞資料,無疑會造成效能問題。

幸運的是,Web Worker 提供了一中轉移資料的方式,允許主執行緒把二進位制資料直接轉移給子執行緒。這種方式比原先拷貝的方式,有巨大的效能提升。

一旦資料轉移到其他執行緒,原先執行緒就無法再使用這些二進位制資料了,這是為了防止出現多個執行緒同時修改資料的麻煩局面

下方栗子出自淺談 HTML5 Web Worker

// 建立二進位制資料
var uInt8Array = new Uint8Array(1024*1024*32); // 32MB
for (var i = 0; i < uInt8Array .length; ++i) {
    uInt8Array[i] = i;
}
console.log(uInt8Array.length); // 傳遞前長度:33554432
// 字串形式建立worker執行緒
var myTask = `
    onmessage = function (e) {
        var data = e.data;
        console.log('worker:', data);
    };
`;

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

// 使用這個格式(a,[a]) 來轉移二進位制資料
myWorker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]); // 傳送資料、轉移資料

console.log(uInt8Array.length); // 傳遞後長度:0,原先執行緒內沒有這個資料了
複製程式碼

二進位制資料有:File、Blob、ArrayBuffer 等型別,也允許在 worker 執行緒之間傳送,這對於影像處理、聲音處理、3D 運算等就非常方便了,不會產生效能負擔

應用場景:

  1. 數學運算

  2. 影像、影音等檔案處理

  3. 大量資料檢索

    比如使用者輸入時,我們在後臺檢索答案,或者幫助使用者聯想,糾錯等操作.

  4. 耗時任務都丟到 webworker 解放我們的主執行緒。

相容:

沒有找到具體的制定日期,有篇部落格是在 10 年的 7 月份寫的,也就是說 web worker 至少出現了八年了,以下相容摘自MDN

Chrome:4, Firefox:3.5, IE:10.0, Opera:10.6, Safari:4

現在相容還是做的比較好的,如果實在不放心的話:

if (window.Worker) {
    ...
}else{
    ...
}
複製程式碼

結語:

Web Worker的出現,給瀏覽器帶來了後臺計算的能力,把耗時的任務分配給worker執行緒來做,在很大程度上緩解了主執行緒UI渲染阻塞的問題,提升頁面效能。

使用起來也不復雜,以後有複雜的問題,記得要丟給我們瀏覽器的後臺(web worker)來處理

看完之後,一定要研究一下文中的栗子,自己鼓搗鼓搗,實踐出真知!

PS: 推薦一下我上個月寫的手摸手教你使用WebSocket,感興趣的可以看一下。

希望看完的朋友可以點個喜歡/關注,您的支援是對我最大的鼓勵。

部落格前端積累文件公眾號GitHub

以上2018.11.25

參考資料:

MDN

Web Worker 使用教程

淺談HTML5 Web Worker

相關文章