web workers简介(三)创建subworker

1,674 阅读4分钟

基础使用

动态内联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