Based on using

Dynamically inlining the worker

subworker


Web Workers (2) Dynamically create workers

Hello everyone, today here is a brief introduction to how to dynamically create inline Web workers.

Create worker dynamically

In introducing worker-Loader, we learned that you can create inline Web workers using Blob and createObjectURL, which makes web workers more flexible to use.

Let’s try this out. Create workify. Js and write our page code:

// index.html
<script type="text/javascript" src="./workify.js"></script>
<script type="text/javascript">
function add(a, b) {
  return a + b;
}
async function initFunc() {
  const workerAdd = workify(add);
  console.log('workerAdd', await workerAdd(23, 16));
}
initFunc();
</script>
Copy the code

We want to create an inline proxy for the Web worker with the workify method, and we can call this method in async/await form.

Next we’ll write our Workify method. Start by adding some utility methods.

// workify.js
(function (g) {
  const toString = function toString(t) {
    return Function.prototype.toString.call(t);
  };
  const getId = function getId() {
    return(+new Date()).toString(32); }; . const workify =function workify(target) {
    ...
  }
  
  g.workify = workify;
})(window);
Copy the code

Next, we’ll create the inline code Web worker:

const code = `(${toString(proxy)}) (${toString(target)}) `; const blob = new Blob([code]); const url = URL.createObjectURL(blob); const worker = new Worker(url);Copy the code

The code we concatenate here will pass the object function as an argument to our proxy function, which will handle the web worker calling the object function and communicating with the main thread (by calling postMessage and setting onMessage).

Next, in Workify we will set the worker’s onMessage method. At the same time, we will add a send method to the worker, which will send a message using postMessage and return a Promise.

Finally, workify returns a method that sends a message via worker.send and returns its Promise:

worker.onmessage = function (ev) {
  //
};
worker.send = function ({ type, data }) {
  //
}
const rtn = functionrtn(... args) {return worker.send({
    type: 'exec',
    data: args,
  });
};
return rtn;
Copy the code

Since we need to know which Promise to resolve when the worker completes the task, we will send an ID in postMessage and the worker will return it:

worker._cbs = {};
worker.onmessage = function (ev) {
  const { type, id, data } = ev.data;
  if (type= = ='exec') { worker._cbs[id](data); }}; worker.send =function ({ type, data }) {
  return new Promise((res) => {
    const id = getId();
    worker._cbs[id] = (data) => {
      res(data);
    };
    worker.postMessage({
      id,
      type,
      data,
    });
  });
}
Copy the code

Then we will implement the proxy method, i.e. the logic on the Web worker side:

const proxy = function proxy(target) {
  self.onmessage = function (ev) {
    const { type, data, id } = ev.data;
    let rtn = null;
    if (type= = ='exec') {
      rtn = target.apply(null, data);
    }
    self.postMessage({
      id,
      type,
      data: rtn,
    });
  };
};
Copy the code

We call the target function with the parameters we receive and send the result and ID back.

If we need to load code through importScripts, we can use importScripts directly in the target function, or we can pass the array of code to load as another parameter to proxy: importScripts

const proxy = function proxy(target, scripts) {
  if(scripts && scripts.length) importScripts.apply(self, scripts); . }Copy the code

As above, we can already inline the function as a Web worker. Next, I would also like to inline the Class as a Web worker as well.

class Adder {
  constructor(initial) {
    this.count = initial;
  }
  add(a) {
    this.count += a;
    return this.count;
  }
}
async function initClass() {
  let WAdder = workify(Adder);
  let instance = await new WAdder(5);
  console.log('apply add', await instance.add(7));
  console.log('get count', await instance.count);
}
initClass();
Copy the code

First, we change the RTN code to see if it passes a new call:

const rtn = functionrtn(... args) {if (this instanceof rtn) {
    return worker.send({
      type: 'create',
      data: args,
    });
  } else {
    return worker.send({
      type: 'exec', data: args, }); }};Copy the code

Next we modify work.onMessage to do different processing depending on the event type (in this case only the CREATE event) :

worker.onmessage = function (ev) {
  const { type, id, data } = ev.data;
  if (type= = ='create') {
    worker._cbs[id](_proxy(worker));
  } else{ worker._cbs[id](data); }};Copy the code

We will first support the following four types of events:

  • Exec: Calls the function
  • Create: Creates an instance
  • Apply: invoke instance methods
  • Get: Obtains instance attributes

The onMessage defined in the corresponding proxy function should also be modified:

self.onmessage = function (ev) {
  const { type, data, id } = ev.data;
  let rtn = null;
  if (type= = ='exec') {
    rtn = target.apply(null, data);
  } else if (type= = ='create') { instance = new target(... data); }else if (type= = ='get') {
    rtn = instance;
    for (letp of data) { rtn = rtn[p]; }}else if (type= = ='apply') {
    rtn = instance;
    for (letp of data.path) { rtn = rtn[p]; } rtn = rtn.apply(instance, data.data); }... };Copy the code

The corresponding logic is to generate an example, get a property, and call a method.

In worker.onMessage, we return a proxy via _proxy(worker), which is a bit of tricky code.

We want the proxy object we return to have access to any fetch property, any calling code, and will invoke the corresponding behavior of the Web worker.

So here we use Proxy, and the target is a function, so we can Proxy both get and apply. In GET, we implement deep proxy recursively using _proxy. We record the current path by path. When the property is then, for example, await instance.count where path is [‘count’], we use worker.send to get the property and return its THEN. If the current path is empty, we can simply return null to indicate that the current object is not Thenable and break the Promise chain.

const _proxy = function _proxy(worker, path) {
  path = path || [];
  return new Proxy(function(){}, {
    get: (_, prop, receiver) => {
      if (prop === 'then') {
        if (path.length === 0) return null;
        const p = worker.send({
          type: 'get',
          data: path,
        });
        return p.then.bind(p);
      }
      return _proxy(worker, path.concat(prop));
    },
    apply: (_0, _1, args) => {
      return worker.send({
        type: 'apply', data: { path, data: args, }, }); }}); };Copy the code

summary

Today you saw how to create inline Web workers via Blob. Next, I’ll show you how to implement something similar to Subworker.

code

(function (g) {
  const toString = function toString(t) {
    return Function.prototype.toString.call(t);
  };
  const getId = function getId() {
    return (+new Date()).toString(32);
  };
  const proxy = function proxy(target, scripts) {
    if (scripts && scripts.length) importScripts.apply(self, scripts);
    let instance;
    self.onmessage = function (ev) {
      const { type, data, id } = ev.data;
      let rtn = null;
      if (type= = ='exec') {
        rtn = target.apply(null, data);
      } else if (type= = ='create') { instance = new target(... data); }else if (type= = ='get') {
        rtn = instance;
        for (letp of data) { rtn = rtn[p]; }}else if (type= = ='apply') {
        rtn = instance;
        for (let p of data.path) {
          rtn = rtn[p];
        }
        rtn = rtn.apply(instance, data.data);
      }
      self.postMessage({
        id,
        type,
        data: rtn,
      });
    };
  };

  const _proxy = function _proxy(worker, path) {
    path = path || [];
    return new Proxy(function(){}, {
      get: (_, prop, receiver) => {
        if (prop === 'then') {
          if (path.length === 0) return null;
          const p = worker.send({
            type: 'get',
            data: path,
          });
          return p.then.bind(p);
        }
        return _proxy(worker, path.concat(prop));
      },
      apply: (_0, _1, args) => {
        return worker.send({
          type: 'apply', data: { path, data: args, }, }); }}); }; const workify =function workify(target, scripts) {
    const code = `(${toString(proxy)}) (${toString(target)}.${JSON.stringify(scripts)}) `; const blob = new Blob([code]); const url = URL.createObjectURL(blob); const worker = new Worker(url); worker._cbs = {}; worker.onmessage =function (ev) {
      const { type, id, data } = ev.data;
      if (type= = ='exec') {
        worker._cbs[id](data);
      } else if (type= = ='create') {
        worker._cbs[id](_proxy(worker));
      } else if (type= = ='apply') {
        worker._cbs[id](data);
      } else if (type= = ='get') { worker._cbs[id](data); }}; worker.send =function ({ type, data }) {
      return new Promise((res) => {
        const id = getId();
        worker._cbs[id] = (data) => {
          res(data);
        };
        worker.postMessage({
          id,
          type,
          data,
        });
      });
    }
    const rtn = functionrtn(... args) {if (this instanceof rtn) {
        return worker.send({
          type: 'create',
          data: args,
        });
      } else {
        return worker.send({
          type: 'exec', data: args, }); }};return rtn;
  };
  g.workify = workify;
})(window);
Copy the code

reference

pshihn/workly