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

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

基礎使用

動態內聯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

相關文章