JS多線程-webworker的基本使用

Web Worker

JavaScript 語言采用的是單線程模型,也就是說,所有任務只能在一個線程上完成,一次只能做一件事。前面的任務沒做完,后面的任務只能等著。隨著電腦計算能力的增強,尤其是多核 CPU 的出現,單線程帶來很大的不便,無法充分發揮計算機的計算能力。

Web Worker 的作用,就是為 JavaScript 創造多線程環境,允許主線程創建 Worker 線程,將一些任務分配給后者運行。在主線程運行的同時,Worker 線程在后臺運行,兩者互不干擾。等到 Worker 線程完成計算任務,再把結果返回給主線程。這樣的好處是,一些計算密集型或高延遲的任務,被 Worker 線程負擔了,主線程(通常負責 UI 交互)就會很流暢,不會被阻塞或拖慢。

Worker 線程一旦新建成功,就會始終運行,不會被主線程上的活動(比如用戶點擊按鈕、提交表單)打斷。這樣有利于隨時響應主線程的通信。但是,這也造成了 Worker 比較耗費資源,不應該過度使用,而且一旦使用完畢,就應該關閉。

  • webWorker創建一個新線程,不會阻塞主線程從而渲染流暢(主線程阻塞會阻塞渲染)
  • WebWorker不會被主線程打斷,但也造成Worker比較耗費資源,一旦使用完畢,就應該關閉
  • 同源限制:分配給Worker線程運行的腳本文件,必須與主線程的腳本文件同源
  • DOM限制:Worker線程沒法操作和讀取主線程所在的DOM對象,也無法使用document、window、parent這些對象,但是Worker線程可以訪問navigator對象和location對象
  • Worker和主線程不能直接通信,需要通過消息完成
  • 腳本限制:不能指向alrt()和confirm方法
  • 文件限制:加載的腳本必須來自網絡,不能為本地文件

API

構造函數:

var myWorker = new Worker(jsUrl, options);

jsUrl是腳本的網址,必須遵循同源策略,并且只能加載JS腳本,否則會報錯。
第二個參數是配置對象:

// 主線程
var myWorker = new Worker('worker.js', { name : 'myWorker' });

// Worker 線程
self.name // myWorker

Worker線程對象的屬性和方法(主線程使用):

  • Worker.onerror:指定 error 事件的監聽函數。
  • Worker.onmessage:指定 message 事件的監聽函數,發送過來的數據在Event.data屬性中
  • Worker.onmessageerror:指定 messageerror 事件的監聽函數。發送的數據無法序列化成字符串時,會觸發這個事件。
  • Worker.postMessage():向 Worker 線程發送消息。
  • Worker.terminate():立即終止 Worker 線程

Worker線程的屬性和方法(worker線程使用):

  • self.name: Worker 的名字。該屬性只讀,由構造函數指定。
  • self.onmessage:指定message事件的監聽函數。
  • self.onmessageerror:指定 messageerror 事件的監聽函數。發送的數據無法序列化成字符串時,會觸發這個事件。
  • self.close():關閉 Worker 線程。
  • self.postMessage():向產生這個 Worker 線程發送消息。
  • self.importScripts():加載 JS 腳本。

基本使用

構造函數:Worker(url),接收一個文件的url,獲取文件來創建一個Worker線程。

var worker = new Worker('work.js')

主線程通過worker.onmessage來接收worker的數據,通過worker.postMessage()來傳遞給worker數據。

worker.postMessage('hello')
worker.onmessage = function(event){
    //event.data為傳過來的數據
    //do something
}
self.onmessage = function(e){
    console.log(e.data)
}
//或者是
self.addEventListener('message',function(e){
    console.log(e.data)
})

通過importScripts方法可以在worker內部加載其他腳本

importScripts('file1','file2')

關閉worker:

//主線程中
worker.terminate()
//worker線程
self.close()

傳值問題

主線程與 Worker 之間的通信內容,可以是文本,也可以是對象。需要注意的是,這種通信是拷貝關系,即是傳值而不是傳址,Worker 對通信內容的修改,不會影響到主線程。事實上,瀏覽器內部的運行機制是,先將通信內容串行化,然后把串行化后的字符串發給 Worker,后者再將它還原。

主線程和Worker之間傳遞的數據是傳值的,而不是傳址的,這樣避免了Worker線程操作主線程的數據,傳遞數據時,會調用內容的toString()方法(二進制數據除外),再傳值,所以對象盡量經過JSON.stringify后傳值

主線程與worker之間可以交換二進制數據,比如File、Blob、ArrayBuffer等類型,也可以在線程之間發送.

// 主線程
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);

// Worker 線程
self.onmessage = function (e) {
  var uInt8Array = e.data;
  postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
  postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};

轉移大文件而不是拷貝

拷貝方式發送二進制數據,會造成性能問題。比如,主線程向 Worker 發送一個 500MB 文件,默認情況下瀏覽器會生成一個原文件的拷貝。為了解決這個問題,JavaScript 允許主線程把二進制數據直接轉移給子線程,但是一旦轉移,主線程就無法再使用這些二進制數據了,這是為了防止出現多個線程同時修改數據的麻煩局面。這種轉移數據的方法,叫做Transferable Objects。這使得主線程可以快速把數據交給 Worker,對于影像處理、聲音處理、3D 運算等就非常方便了,不會產生性能負擔


// Transferable Objects 格式
worker.postMessage(aMessage, transferList);

// 例子
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);

  • transferList:數組,用于傳遞所有權,如果一個對象的所有權被轉義,在發送它的上下文中將變為不可用,可轉義對象必須是ArrayBuffer、MessagePort(MessageChanel的port)或者ImageBitmap的實例。

在頁面內創建Web Worker

通常情況下,Worker 載入的是一個單獨的 JavaScript 腳本文件,但是也可以載入與主線程在同一個網頁的代碼。

通過script標簽

<body>
    <script id="worker" type="app/worker">
      addEventListener('message', function () {
        postMessage('some message');
      }, false);
    </script>
</body>

上面是一段嵌入網頁的腳本,注意必須指定<script>標簽的type屬性是一個瀏覽器不認識的值,上例是app / worker。

//基于script的內容創建一個Blob對象
var blob = new Blob([document.querySelector('#worker').textContent]);
// 生成指向Blob對象的blobURL
var url = window.URL.createObjectURL(blob);
// 請求blobURL來創建worker
var worker = new Worker(url);

worker.onmessage = function (e) {
  // e.data === 'some message'
};

worker線程完成輪詢

有時,瀏覽器需要輪詢服務器狀態,以便第一時間得知狀態改變。這個工作可以放在 Worker 里面。

function createWorker(f) {
  var blob = new Blob(['(' + f.toString() +')()']);
  var url = window.URL.createObjectURL(blob);
  var worker = new Worker(url);
  return worker;
}

var pollingWorker = createWorker(function (e) {
  var cache;

  function compare(new, old) { ... };

  setInterval(function () {
    fetch('/my-api-endpoint').then(function (res) {
      var data = res.json();

      if (!compare(data, cache)) {
        cache = data;
        self.postMessage(data);
      }
    })
  }, 1000)
});

pollingWorker.onmessage = function () {
  // render data
}

pollingWorker.postMessage('init');

一個完整的使用例子:

 function createWorker(fn) {
    var blob = new Blob([`(${fn.toString()})()`])
    var blobURL = window.URL.createObjectURL(blob)
    var worker = new Worker(blobURL)
    return worker
}
var webWorker = createWorker(function () {
    function init() {
        let i = 0
        setInterval(function () {
            self.postMessage(++i)
        }, 1000)
    }
    self.onmessage = function (messageEvent) {
        switch (messageEvent.data) {
            case "init":
                init()
                break
            case "stop":
                console.log("close!");
                self.close();
                break;
        }

    }
})
webWorker.onmessage = function (event) {
    console.log(event.data)
    if (event.data === 5) {
        webWorker.postMessage("stop")
        //主線程關閉webWorker.terminate()
    }
}
webWorker.postMessage("init")

MessageChannel

Vue的$nextTick實現中,優先檢測是否支持原生的setImmediate(Node和高版本IE\chrome支持),不支持的話檢測是否支持原生的MessageChannel,如果再不支持就會降級為setTimeout.

setImmediate\MessageChannel\setTimeout都屬于宏任務,宏任務就是等待主線程空閑后才會被執行的任務(宏任務->微任務->渲染->宏任務的執行過程),實現宏任務效果最理想的就是setImmediate,MessageChannel也可以替代,但setImmediate和MessageChannel的瀏覽器支持沒有setTimeout好,setTimeout有一個致命缺點就是即使設置setTimeout(fn,0),fn不會立即被推入到宏任務隊列中,在chrome中至少是4ms以上。而 setImmediate可以將任務直接推入到宏任務隊列

MessageChannle允許我們創建一個新的消息通道,并通過它的兩個MessagePort屬性發送數據。

var channel = new MessageChannel()
//兩個端口
channel.port1
channel.port2

MessageChannel創建了一個通信的管道,這個管道有兩個端口,每個端口都可以通過postMessage發送數據,而一個端口只要綁定了onmessage回調方法,就可以接收從另一個端口傳過來的數據。(這一點和Web Worker類似,傳遞的數據從event.data獲取)

var channel = new MessageChannel();
var port1 = channel.port1;
var port2 = channel.port2;
port1.onmessage = function(event) {
    console.log("port1收到來自port2的數據:" + event.data);
}
port2.onmessage = function(event) {
    console.log("port2收到來自port1的數據:" + event.data);
}

port1.postMessage("發送給port2");
port2.postMessage("發送給port1");

多個worker之間的通信

當我們使用多個Web Worker并想要在兩個Web Worker之間實現通信的時候,MessageChannel可以派上用場:

//主線程
var w1 = new Worker("worker1.js");
var w2 = new Worker("worker2.js");
var ch = new MessageChannel();
//由于直接傳遞會被轉化成字符串,這里使用轉移數據的方法
//MessageChannel會被自動存儲在event.ports里面
w1.postMessage("port1", [ch.port1]);
w2.postMessage("port2", [ch.port2]);
w2.onmessage = function(e) {
    console.log(e.data);
}
//worker1
self.onmessage = function(e) {
    const  port = e.ports[0];
    //傳遞給worker2
    port.postMessage("this is from worker1")        
}
//worker2
onmessage = function(e) {
    const  port = e.ports[0];
    //接受worker1消息
    port.onmessage = function(e){
        //傳遞回主線程
        self.postMessage(e.data)
    }        
}

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容