JS多線程-webworker的基本使用

Web Worker

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

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

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

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

API

構造函數(shù):

var myWorker = new Worker(jsUrl, options);

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

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

// Worker 線程
self.name // myWorker

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

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

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

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

基本使用

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

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

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

worker.postMessage('hello')
worker.onmessage = function(event){
    //event.data為傳過來的數(shù)據(jù)
    //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 對通信內容的修改,不會影響到主線程。事實上,瀏覽器內部的運行機制是,先將通信內容串行化,然后把串行化后的字符串發(fā)給 Worker,后者再將它還原。

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

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

// 主線程
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);
};

轉移大文件而不是拷貝

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


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

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

  • transferList:數(shù)組,用于傳遞所有權,如果一個對象的所有權被轉義,在發(fā)送它的上下文中將變?yōu)椴豢捎?,可轉義對象必須是ArrayBuffer、MessagePort(MessageChanel的port)或者ImageBitmap的實例。

在頁面內創(chuàng)建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的內容創(chuàng)建一個Blob對象
var blob = new Blob([document.querySelector('#worker').textContent]);
// 生成指向Blob對象的blobURL
var url = window.URL.createObjectURL(blob);
// 請求blobURL來創(chuàng)建worker
var worker = new Worker(url);

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

worker線程完成輪詢

有時,瀏覽器需要輪詢服務器狀態(tài),以便第一時間得知狀態(tài)改變。這個工作可以放在 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實現(xiàn)中,優(yōu)先檢測是否支持原生的setImmediate(Node和高版本IE\chrome支持),不支持的話檢測是否支持原生的MessageChannel,如果再不支持就會降級為setTimeout.

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

MessageChannle允許我們創(chuàng)建一個新的消息通道,并通過它的兩個MessagePort屬性發(fā)送數(shù)據(jù)。

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

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

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

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

多個worker之間的通信

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

//主線程
var w1 = new Worker("worker1.js");
var w2 = new Worker("worker2.js");
var ch = new MessageChannel();
//由于直接傳遞會被轉化成字符串,這里使用轉移數(shù)據(jù)的方法
//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)
    }        
}

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容