web workers簡介(三)建立subworker

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

基礎使用

動態內聯worker

subworker


web workers簡介(三)subworker

大家好,今天在這裡簡單介紹一下如何實現與subworker相似的功能。

subworker

在web workers的標準中,還有一個概念叫subworker,這使得你可以在web workers中建立web workers。但事實上如果你在例如chromesafari瀏覽器中這麼做,會得到類似Worker is not defined這樣的報錯資訊,即無法在web worker內建立Worker。即使這是一個自2010年即被報告的bug,但仍始終未被修復,因此如果你需要使用subworker,只能藉助其他手段來實現類似的效果。

這裡我們需要將web workers的建立(以及web workers之間的通訊)通過主執行緒進行代理。

假設我們建立一個web worker(稱為parent),併為p建立一個subworkerchild,加上主執行緒main,如果我們需要實現從主執行緒到worker再到subworker並返回的通訊歷程,即:

  • m2p
  • p2c
  • c2p
  • p2m

其中比較需要關注的是2、3即parent和child之間的通訊,這裡實際都是傳送資訊給main之後讓main來轉發資訊的。

首先,我們判斷當前程式碼在worker還是主執行緒中執行。因為web worker的種種限制,判斷方式有多種,例如嘗試呼叫document,或是嘗試呼叫Worker

(function () {
  let inWorker = false;
  try {
    document;
  } catch (_) {
    inWorker = true;
  }

  const getId = function getId() {
    return (+new Date()).toString(32);
  };

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

主執行緒中的程式碼如下:

// main.js
const parent = new SubWorker('parent.js');
parent.postMessage('start');
parent.onmessage = (ev) => {
  if (ev.data === 'success'){
    console.log('p 2 m');
  }
};
複製程式碼

在主執行緒的環境下,我們的SubWorker只是對真正的worker做一層簡單的代理,例如postMessageterminate都是直接呼叫真正的worker來執行操作。同時,我們把所有worker都通過id儲存下引用:

const workers = {};

class SubWorker {
  constructor (f, id, parentId) {
    this.id = id || getId();
    this.worker = new Worker(f);
    this.worker.onmessage = this.handleMessage.bind(this);
    this.parentId = parentId;
    workers[this.id] = this;
  }
  handleMessage(ev) {
    ...
  }
  postMessage(data) {
    this.worker.postMessage(data);
  }
  terminate() {
    this.worker.terminate();
  }
}

self.SubWorker = SubWorker;
複製程式碼

這裡m2p的訊息傳送已經實現了。

在建立worker時我們指定了onmessage。如果我們收到的是帶_subWorker標誌的訊息,則代表我們需要處理worker內為child worker代理的事件,包括新建worker、傳送訊息和終止worker等等。在新建child worker時,parent會指定id併傳送過來,這樣在main和parent中就是用同一個id標記實際的worker和它的代理,這樣我們就能讓main為parent代理child的事件傳送(data.type === 'msg')。同時,接收到指令的parent worker和由此建立的child worker的父子關係也被需要記錄下來:

handleMessage(ev) {
  const data = ev.data;
  if (!data._subWorker) {
    ...
  }
  if (data.type === 'create') {
    const subWorker = new SubWorker(data.file, data.id, this.id);
  } else if (data.type === 'msg') {
    workers[data.id].postMessage(data.data);
  } else if (data.type === 'terminate') {
    workers[data.id].terminate();
  }
}
複製程式碼

接下來我們再來看一下在worker中的SubWorkerp2m訊息使用的是原生的postMessage來傳送的。parent worker接受訊息使用了onMessage而避開了原生了onmessage,這裡的原因之後會解釋的:

// parent.js
importScripts('./subworkers.js');

const subWorker = new SubWorker('child.js');

subWorker.onmessage = (ev) => {
  if (ev.data === 'pong') {
    console.log('c 2 p');
    postMessage('success');
  }
};

onMessage = (ev) => {
  if (ev.data === 'start'){
    console.log('m 2 p');
    subWorker.postMessage('ping');
  }
};
複製程式碼

在worker中的SubWorker只需要實現一些簡單的代理,傳送帶有_subWorker標誌位的訊息給主執行緒:

if (!self.SubWorker) {
  const workers = {};

  class SubWorker {
    constructor (f) {
      this.id = getId();
      workers[this.id] = this;
      self.postMessage({
        _subWorker: true,
        type: 'create',
        id: this.id,
        file: f,
      });
    }
    postMessage(data) {
      self.postMessage({
        _subWorker: true,
        type: 'msg',
        id: this.id,
        data: data,
      });
    }
    terminate() {
      self.postMessage({
        _subWorker: true,
        type: 'terminate',
        id: this.id,
      });
    }
  }

  self.SubWorker = SubWorker;
}
複製程式碼

parent中SubWorkerpostMessage發出的訊息被主程式轉發給child worker,child worker用原生的onmessagepostMessage來收發訊息,這樣p2c的訊息傳送也完成了,而c2p的訊息傳送需要進一步的處理。

c2p的訊息會在主執行緒中接收到,但這條訊息是傳送給parent而非main的,因此這裡需要轉發一下。當main中的SubWorker收到訊息時,如果當前的SubWorker沒有父級,那訊息就是發給自己的,否則,實際的訊息接受人應該是自己的父級worker,因此需要轉發一下訊息。於是我們需要修改主執行緒下SubWorkerhandleMessage方法:

handleMessage(ev) {
  const data = ev.data;
  if (!data._subWorker) {
    if (this.parentId) {
      workers[this.parentId].postMessage({
        _subWorker: true,
        type: 'msg',
        id: this.id,
        data: data,
      });
    } else {
      this.onmessage(ev);
    }
    return;
  }
  
  ...
}
複製程式碼

最後的部分是,處理m2pc2p兩種訊息的接收。這兩個訊息都是在parent中接收的,一個是self.onMessage,一個是subWorker.onmessage。但接收訊息的渠道只有self.onmessage(或者通過self.addEventListener('message', cb)),因此需要設定self.onmessage,並將訊息交給自己或subWorker處理:

if (inWorker) {
  if (!self.SubWorker) {
    const workers = {};

    self.onmessage = function onmessage(ev) {
      const data = ev.data;
      if (!data._subWorker) {
        self.onMessage(ev);
        return;
      }
      workers[data.id].onmessage(new MessageEvent('worker', {
        data: data.data,
      }));
    };

    ...

  }
}
複製程式碼

如此,整個訊息的接收、傳送流程就走通了。

小結

自己動手實現一下SubWorker,對於實踐和鞏固代理模式的知識會是非常好的場景。

總結

web workers是一個非常酷的東西,包括從未被實現的subworker、因為安全漏洞被關閉的SharedArrayBuffer等等特性。相比它的好哥們兒Service Workers的C位出道,隨著wasm越來越完善,也許web workers還沒火就要過氣了 (:

程式碼

(function () {
  let inWorker = false;
  try {
    document;
  } catch (_) {
    inWorker = true;
  }

  const getId = function getId() {
    return (+new Date()).toString(32);
  };

  if (inWorker) {
    if (!self.SubWorker) {
      const workers = {};

      self.onmessage = function onmessage(ev) {
        const data = ev.data;
        if (!data._subWorker) {
          self.onMessage(ev);
          return;
        }
        workers[data.id].onmessage(new MessageEvent('worker', {
          data: data.data,
        }));
      };

      class SubWorker {
        constructor (f) {
          this.id = getId();
          workers[this.id] = this;
          self.postMessage({
            _subWorker: true,
            type: 'create',
            id: this.id,
            file: f,
          });
        }
        postMessage(data) {
          self.postMessage({
            _subWorker: true,
            type: 'msg',
            id: this.id,
            data: data,
          });
        }
        terminate() {
          self.postMessage({
            _subWorker: true,
            type: 'terminate',
            id: this.id,
          });
        }
      }

      self.SubWorker = SubWorker;
    }
  } else {
    const workers = {};

    class SubWorker {
      constructor (f, id, parentId) {
        this.id = id || getId();
        this.worker = new Worker(f);
        this.worker.onmessage = this.handleMessage.bind(this);
        this.parentId = parentId;
        workers[this.id] = this;
      }
      handleMessage(ev) {
        const data = ev.data;
        if (!data._subWorker) {
          if (this.parentId) {
            workers[this.parentId].postMessage({
              _subWorker: true,
              type: 'msg',
              id: this.id,
              data: data,
            });
          } else {
            this.onmessage(ev);
          }
          return;
        }
        if (data.type === 'create') {
          const subWorker = new SubWorker(data.file, data.id, this.id);
        } else if (data.type === 'msg') {
          workers[data.id].postMessage(data.data);
        } else if (data.type === 'terminate') {
          workers[data.id].terminate();
        }
      }
      postMessage(data) {
        this.worker.postMessage(data);
      }
      terminate() {
        this.worker.terminate();
      }
    }

    self.SubWorker = SubWorker;
  }
})();
複製程式碼

參考

dmihal/Subworkers

相關文章