web workers簡介(二)動態建立worker

基礎使用javascript

動態內聯workerhtml

subworkerjava


web workers簡介(二)動態建立worker

你們好,今天在這裏簡單介紹一下如何動態的建立內聯的web workers。git

動態建立worker

在介紹worker-loader時,咱們已經瞭解到能夠經過BlobcreateObjectURL來建立內聯的web worker,這使得web worker的使用變得更加靈活。github

接下來,讓咱們一塊兒來對此進行嘗試。新建workify.js,而且編寫咱們的頁面代碼:web

// 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方法。首先添加一些工具方法。bash

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

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)。async

接下來,在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

相關文章
相關標籤/搜索