淺談HTML5 Web Worker

考拉海購前端團隊發表於2017-09-20

前言

眾所周知,Javascript是執行在單執行緒環境中,也就是說無法同時執行多個指令碼。假設使用者點選一個按鈕,觸發了一段用於計算的Javascript程式碼,那麼在這段程式碼執行完畢之前,頁面是無法響應使用者操作的。但是,如果將這段程式碼交給Web Worker去執行的話,那麼情況就不一樣了:瀏覽器會在後臺啟動一個獨立的worker執行緒來專門負責這段程式碼的執行,因此,頁面在這段Javascript程式碼執行期間依然可以響應使用者的其他操作。

Web Worker簡介

Web Worker 是HTML5標準的一部分,這一規範定義了一套 API,它允許一段JavaScript程式執行在主執行緒之外的另外一個執行緒中。
值得注意的是, Web Worker 規範中定義了兩類工作執行緒,分別是專用執行緒Dedicated Worker和共享執行緒 Shared Worker,其中,Dedicated Worker只能為一個頁面所使用,而Shared Worker則可以被多個頁面所共享。

快速上手

建立worker

只需呼叫Worker() 建構函式並傳入一個要在 worker 執行緒內執行的指令碼的URI,即可建立一個新的worker。


var myWorker = new Worker("my_task.js");

// my_task.js中的程式碼 
var i = 0;
function timedCount(){
    i = i+1;
    postMessage(i);
    setTimeout(timedCount, 1000);
}
timedCount();複製程式碼

另外,通過URL.createObjectURL()建立URL物件,可以實現建立內嵌的worker


var myTask = `
    var i = 0;
    function timedCount(){
        i = i+1;
        postMessage(i);
        setTimeout(timedCount, 1000);
    }
    timedCount();
`;

var blob = new Blob([myTask]);
var myWorker = new Worker(window.URL.createObjectURL(blob));複製程式碼

這樣,就可以結合NEJ、Webpack進行模組化管理、打包了。

注意:傳入 Worker 建構函式的引數 URI 必須遵循同源策略。Worker執行緒的建立的是非同步的,主執行緒程式碼不會阻塞在這裡等待worker執行緒去載入、執行指定的指令碼檔案,而是會立即向下繼續執行後面程式碼。

提示:本文所有的示例程式碼均可直接拷貝到chrome控制檯中執行。

Worker執行緒資料通訊方式

Worker 與其主頁面之間的通訊是通過 onmessage 事件和 postMessage() 方法實現的。

在主頁面與 Worker 之間傳遞的資料是通過拷貝,而不是共享來完成的。傳遞給 Worker 的物件需要經過序列化,接下來在另一端還需要反序列化。頁面與 Worker 不會共享同一個例項,最終的結果就是在每次通訊結束時生成了資料的一個副本。

也就是說,Worker 與其主頁面之間只能單純的傳遞資料,不能傳遞複雜的引用型別:如通過建構函式建立的物件等。並且,傳遞的資料也是經過拷貝生成的一個副本,在一端對資料進行修改不會影響另一端。

var myTask = `
    onmessage = function (e) {
        var data = e.data;
        data.push('hello');
        console.log('worker:', data); // worker: [1, 2, 3, "hello"]
        postMessage(data);
    };
`;

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

myWorker.onmessage = function (e) {
    var data = e.data;
    console.log('page:', data); // page: [1, 2, 3, "hello"]
    console.log('arr:', arr); // arr: [1, 2, 3]
};

var arr = [1,2,3];
myWorker.postMessage(arr);複製程式碼

通過可轉讓物件來傳遞資料

前面介紹了簡單資料的傳遞,其實還有一種效能更高的方法來傳遞資料,就是通過可轉讓物件將資料在主頁面和Worker之間進行來回穿梭。可轉讓物件從一個上下文轉移到另一個上下文而不會經過任何拷貝操作。這意味著當傳遞大資料時會獲得極大的效能提升。和按照引用傳遞不同,一旦物件轉讓,那麼它在原來上下文的那個版本將不復存在。該物件的所有權被轉讓到新的上下文內。例如,當你將一個 ArrayBuffer 物件從主應用轉讓到 Worker 中,原始的 ArrayBuffer 被清除並且無法使用。它包含的內容會(完整無差的)傳遞給 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

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));
myWorker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);

console.log(uInt8Array.length); // 傳遞後長度:0複製程式碼

importScripts()

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

瀏覽器載入並執行每一個列出的指令碼,每個指令碼中的全域性物件都能夠被 worker 使用。如果指令碼無法載入,將丟擲 NETWORK_ERROR 異常,接下來的程式碼也無法執行。而之前執行的程式碼(包括使用 window.setTimeout() 非同步執行的程式碼)依然能夠執行。importScripts() 之後的函式宣告依然會被保留,因為它們始終會在其他程式碼之前執行。

注意:指令碼的下載順序不固定,但執行時會按照傳入 importScripts() 中的檔名順序進行。這個過程是同步完成的;直到所有指令碼都下載並執行完畢, importScripts() 才會返回。

Worker上下文

Worker執行的上下文,與主頁面執行時的上下文並不相同,最頂層的物件並不是window,而是個一個叫做WorkerGlobalScope的東東,所以無法訪問window、以及與window相關的DOM API,但是可以與setTimeout、setInterval等協作。

WorkerGlobalScope作用域下的常用屬性、方法如下:

1、self

我們可以使用 WorkerGlobalScope 的 self 屬性來或者這個物件本身的引用

2、location

  location 屬性返回當執行緒被建立出來的時候與之關聯的 WorkerLocation 物件,它表示用於初始化這個工作執行緒的腳步資源的絕對 URL,即使頁面被多次重定向後,這個 URL 資源位置也不會改變。

3、close

  關閉當前執行緒

4、importScripts

  我們可以通過importScripts()方法通過url在worker中載入庫函式

5、XMLHttpRequest

  有了它,才能發出Ajax請求

6、setTimeout/setInterval以及addEventListener/postMessage

終止 terminate()

在主頁面上呼叫terminate()方法,可以立即殺死 worker 執行緒,不會留下任何機會讓它完成自己的操作或清理工作。另外,Worker也可以呼叫自己的 close() 方法來關閉自己


    // 主頁面呼叫
    myWorker.terminate();

    // Worker 執行緒呼叫
    self.close();複製程式碼

處理錯誤

當 worker 出現執行時錯誤時,它的 onerror 事件處理函式會被呼叫。它會收到一個實現了 ErrorEvent 介面名為 error的事件。該事件不會冒泡,並且可以被取消;為了防止觸發預設動作,worker 可以呼叫錯誤事件的 preventDefault() 方法。

錯誤事件有三個實用的屬性:filename - 發生錯誤的指令碼檔名;lineno - 出現錯誤的行號;以及 message - 可讀性良好的錯誤訊息。


var myTask = `
    onmessage = function (e) {
        var data = e.data;
        console.log('worker:', data);
    };

    // 使用未宣告的變數
    arr.push('error');
`;

var blob = new Blob([myTask]);
var myWorker = new Worker(window.URL.createObjectURL(blob));
myWorker.onerror = function onError(e) {
    // ERROR: Line 8 in blob:http://www.cnblogs.com/490a7c32-7386-4d6e-a82b-1ca0b1bf2469: Uncaught ReferenceError: arr is not defined
    console.log(['ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message].join(''));
}複製程式碼

總結

最後總結下Web Worker為javascript帶來了什麼,以及典型的應用場景。

強大的計算能力

可以載入一個JS進行大量的複雜計算而不掛起主程式,並通過postMessage,onmessage進行通訊,解決了大量計算對UI渲染的阻塞問題。

典型應用場景

1、數學運算

Web Worker最簡單的應用就是用來做後臺計算,對CPU密集型的場景再適合不過了。

2、影像處理

通過使用從<canvas>中獲取的資料,可以把影像分割成幾個不同的區域並且把它們推送給並行的不同Workers來做計算,對影像進行畫素級的處理,再把處理完成的影像資料返回給主頁面。

3、大資料的處理

目前mvvm框架越來越普及,基於資料驅動的開發模式也越愈發流行,未來大資料的處理也可能轉向到前臺,這時,將大資料的處理交給在Web Worker也是上上之策了吧。

相關文章