web workers簡介(一)基礎使用

笨笨小撒發表於2018-07-31

基礎使用

動態內聯worker

subworker


web workers簡介(一)基礎使用

大家好,今天在這裡簡單介紹一下web workers的基本使用。

web workers

web workers可以使得一些涉及複雜計算的邏輯在獨立的執行緒執行,從而不會影響頁面的效能,例如渲染、互動響應等。

web workers中可以使用大多數標準的JS特性,例如XMLHttpRequest等;當然web workers也有不少侷限行,例如不能操作DOM、作用域是獨立的等等。

基本使用

下面來一起看一個簡單的web workers例子。

我們首先建立一個worker.js的空檔案。我們將在其中編寫worker的程式碼。

然後我們通過new Worker(workerFilePath)的方式建立一個worker。

// index.html
const worker = new Worker('./worker.js');
複製程式碼

主執行緒和worker通過postMessageonmessage來通訊。

首先,我們在主執行緒中通過worker.postMessage向worker傳送訊息。

// index.html
worker.postMessage('start');
複製程式碼

接著,我們在worker.js中定義onmessage來處理接收到的訊息:

// worker.js
onmessage = function (ev) {
  if (ev.data === 'start') {
    timedCount();
  }
};
複製程式碼

onmessage會收到一個MessageEvent事件作為引數,其中的data屬性就是我們在postMessage中傳送的資料。

同樣的,我們可以在worker.js中使用postMessage向主執行緒傳送訊息:

// worker.js
const timedCount = function timedCount() {
  i += 1;
  postMessage(i);
  setTimeout(timedCount, 500);
};
複製程式碼

在主執行緒中我們通過worker.onmessage接受worker發來的訊息,並可以呼叫worker.terminate來終止worker

// index.html
worker.onmessage = function onmessage(ev) {
  document.getElementById('result').innerHTML = ev.data;
  if (ev.data === 10) worker.terminate();
};
複製程式碼

當然,我們可以使用worker.js建立多個worker:

// index.html
const worker1 = new Worker('./worker.js');
const worker2 = new Worker('./worker.js');
const anotherWorker = new Worker('./anotherWorker.js');
複製程式碼

importScript

我們可以使用importScript在web workers中引入其它程式碼,importScripts是同步載入程式碼的,載入的程式碼中暴露的全域性物件能夠被worker使用,例如:

// worker.js
importScripts('./foo.js', './bar.js');

foo();
bar();

// foo.js
function foo() {
  console.log('foo');
};

// bar.js
function bar() {
  console.log('bar.js');
};

複製程式碼

注意這裡引入程式碼的路徑是與當前worker程式碼所在的路徑對應的。

可轉讓物件(Transferable Objects)

由於在主執行緒和web workers之間傳遞資料需要經過拷貝過程,因此當我們在postMessage中傳送大型資料時效能會收到影響。而可轉讓物件使得資料在上下文切換的過程中不必經過任何拷貝操作,從而大大提高了效能。

當資料被轉移後,在原上下文中將被清除而不復存在:

// index.html
const worker = new Worker('./js/arrayBuffer.js');
const uInt8Array = new Uint8Array(1024); 
for (var i = 0; i < uInt8Array .length; ++i) {
  uInt8Array[i] = i;
}
worker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]);
console.log(uInt8Array.byteLength); // 0

// arrayBuffer.js
onmessage = function (ev) {
  const uInt8View = new Uint8Array(ev.data);
  console.log(uInt8View.byteLength, uInt8View[11]);
};
複製程式碼

共享記憶體(Shared Memory)

共享記憶體是另一個使得主執行緒與web workers之前可以高效傳遞(共享)資料的方式。SharedArrayBuffer便是其實現。不過由於安全問題這一特性最終被關閉。

在webbpack中使用web worker

我們可以通過worker-loader來在webpack中使用web worker。

import Worker from 'worker-loader!./worker.js';

const worker = new Worker();
worker.postMessage('start');
worker.onmessage = function(ev) {};
複製程式碼

隨後我們打包程式碼,在打包後的程式碼中我們應該可以看到類似下面的程式碼:

module.exports = function() {
  return new Worker(__webpack_require__.p + "61a1c1b143c4403de10b.worker.js");
};
複製程式碼

在這裡可以看到worker-loader使用new Worker(url)的方式來載入web worker。

如果我們將以上的程式碼修改為:

import Worker from 'worker-loader?inline=true&fallback=false!./worker.js';
複製程式碼

並重新打包。這次我們將在vendor.js(根據你的配置也可能是其他檔名)中找到類似如下的程式碼:

module.exports = function (content, url) {
  try {
    try {
      var blob;

      try {
        // BlobBuilder = Deprecated, but widely implemented
        var BlobBuilder = window.BlobBuilder || window.WebKitBlobBuilder || window.MozBlobBuilder || window.MSBlobBuilder;

        blob = new BlobBuilder();

        blob.append(content);

        blob = blob.getBlob();
      } catch (e) {
        // The proposed API
        blob = new Blob([content]);
      }

      return new Worker(URL.createObjectURL(blob));
    } catch (e) {
      return new Worker('data:application/javascript,' + encodeURIComponent(content));
    }
  } catch (e) {
    if (!url) {
      throw Error('Inline worker is not supported');
    }

    return new Worker(url);
  }
};
複製程式碼

這裡worker-loader會嘗試通過new Blob以及URL.createObjectURL來將worker的程式碼內聯,這裡的content就是相應worker的程式碼文字。當然如果失敗還是會回退為使用url載入web worker。

Shared worker

Shared worket提供了一種跨視窗/iframe等通訊的途徑。接下來我們用chrome來執行一下demo。

// index.html
<script type="text/javascript">
const id = (+new Date()).toString(32);

const myWorker = new SharedWorker('./js/shared.js');
myWorker.port.start();

myWorker.port.onmessage = function(ev) {
  const { type, id, data } = ev.data;
  switch(type) {
    case 'created':
      console.log(data);
      break;
  }
};

myWorker.port.postMessage({
  id,
  type: 'start',
});
</script>
複製程式碼

在頁面上我們使用(+new Date()).toString(32)來將當前時間戳轉換而成的字串作為id(這裡我們假設它的唯一性是有保障的)。

然後我們通過new SharedWorker(filePath)來載入shared worker,並且用myWorker.port.start()啟動它。這會觸發shared worker的onconnect

接著我們通過port.onmessage設定訊息接收回撥,並使用port.postMessage向其發出一條訊息。

const ids = [];
const ports = [];

const broadcast = function (data) {
  for (let port of ports) {
    port.postMessage(data);
  }
};

onconnect = function(e) {
  const port = e.ports[0];
  ports.push(port);

  port.addEventListener('message', (ev) => {
    const { type, id } = ev.data;
    switch(type) {
      case 'start':
        ids.push(id);
        broadcast({
          id,
          type: 'created',
          data: ids,
        });
        break;
    }
  });

  port.start();
};
複製程式碼

每當有shared worker接入,我們都在onconnect中將對應的port儲存下來,這樣我們就能實現廣播機制。

我們通過port.addEventListener('message', handler)的方式新增接受訊息的偵聽,之後用port.start()啟動它。

當我們收到start訊息後,我們將新接入的通道的id儲存下來,並這一訊息廣播出來(傳送給所有已連結的ports)。

如果我們先後在chrome瀏覽器中開啟多個tab並開啟我們的頁面,就能看到新通道接入的訊息被廣播了。通過這樣的方式,我們就能在多個視窗/iframe等之間實現通訊了。

小結

今天介紹了web workers的基本使用,接下來要介紹一下如何動態的建立內聯的web workers。

參考

web workers MDN

相關文章