web workers简介(二)动态创建worker

2,408 阅读5分钟

基础使用

动态内联worker

subworker


web workers简介(二)动态创建worker

大家好,今天在这里简单介绍一下如何动态的创建内联的web workers。

动态创建worker

在介绍worker-loader时,我们已经了解到可以通过BlobcreateObjectURL来创建内联的web worker,这使得web worker的使用变得更加灵活。

接下来,让我们一起来对此进行尝试。新建workify.js,并且编写我们的页面代码:

// 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>

我们希望通过workify方法创建一个内联的web worker的代理,并且可以用async/await的形式来调用这个方法。

接着我们将编写我们的workify方法。首先添加一些工具方法。

// 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);

接下来,我们将创建内联代码的web worker:

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

这里我们拼接的代码,将会把目标函数作为参数传给我们的proxy函数,proxy函数会负责处理web worker调用目标函数并与主线程进行通信(通过调用postMessage和设置onmessage)。

接下来,在workify中我们将设置workeronmessage方法。同时我们将为worker添加一个send方法,这个方法会使用postMessage发出消息,并返回一个Promise

最后workify会返回一个方法,这个方法会通过worker.send发送消息并返回它的Promise

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

因为我们需要在worker完成任务时知道需要去resolve哪个Promise,因此我们将在postMessage中发送一个id,并由worker再返回来:

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,
    });
  });
}

之后我们再来实现proxy方法,既web worker端的逻辑:

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,
    });
  };
};

我们使用接收到的参数来调用目标函数,并将结果和id发送回去。

如果需要通过importScripts加载代码,我们可以在目标函数中直接使用importScripts,或将需要加载的代码数组作为另一个参数传入proxy:

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

如上,我们已经可以将函数内联为web worker。接下来,我还希望能将Class同样内联为web worker。

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();

首先,我们改变rtn的代码,以判断其是否通过new调用:

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

接下来我们修改work.onmessage,根据事件类型做出不同处理(在此处不同的仅create事件):

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

我们将先支持以下4类事件:

  • exec:调用函数
  • create:创建实例
  • apply:调用实例方法
  • get:获取实例属性

相应的proxy函数中定义的onmessage也要修改:

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 (let p 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);
  }
  
  ...

};

对应的逻辑分别为生成示例、获取属性与调用方法。

worker.onmessage中,我们通过_proxy(worker)来返回一个代理,这是比较tricky的一段代码。

我们希望我们返回的代理对象,可以获得任意获取属性、任意调用代码,并将调用web worker相应的行为。

因此这里我们使用了Proxy,并且其目标是一个函数,这样我们就能代理期get(获取属性)和apply(调用)两种行为。在get中,我们通过递归的使用_proxy来实现深度代理。我们通过path来记录当前路径,当获取的属性为then时,例如await instance.countpath['count'],我们将使用worker.send来获取相应的属性并返回其then;而若当前path为空,我们可以直接返回null,表示当前对象非thenable并中断Promise链。

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,
        },
      });
    },
  });
};

小结

今天介绍了如何通过Blob来创建内联的web workers。接下来将要介绍一下如何实现与subworker相似的功能。

代码

(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 (let p 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 = function rtn(...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);

参考

pshihn/workly