WebWorker、使ってますか?
「JavaScriptはシングルスレッド処理だから」なんてもう言わせない。
JavaScriptでマルチスレッド処理を実現する素晴らしい機能です。
データ処理をワーカースレッドへ回すことにより、メインスレッドでのDOM描画をブロッキングしないので、重い処理を実装するときは、とても重要な機能となります。
実行エンジン側でスレッド管理をしてくれるので、スレッドセーフについても気にする必要はありません。
そんな良いことだらけのWebWorkerですが、見方によってはひとつ弱点があります。
WebWorkerを使用するためのWorker
コンストラクタには.js
ファイルへの "パス" が必要なのです。
そう、ワーカースレッドで実行されるコードは、別ファイルとして用意しなければなりません。
しかし、プロトタイピングなどちょっとした用事で都度ファイルを用意するのは、とても面倒です。
そこで、WebWorkerをインラインで気軽にサクッと書いて使える方法を考えてみました。
解決策
まず、ワーカースレッドで実行したいコードをメソッドとして普通に書きます。
普通にメソッドを書いてるだけなので、エディタのシンタックスハイライトやサジェストも問題なく使えます。
function runByWorker(){
self.addEventListener("message", ({data})=>{
self.postMessage(`Worker Received: ${data}`);
});
}
なお、このコードはあくまでワーカースレッドで実行されるので、DOM操作は出来ません。
次に、メインスレッドでWorker
コンストラクタへJavaScriptコードを直接代入できるWebWorkerの拡張クラスを作ります。
class WorkerInline extends Worker{
#ctx = "";
constructor(src){
if(!(typeof src === "function" || typeof src === "string")){
throw new Error("argument must be 'function' or 'string'");
}
const ctx = URL.createObjectURL(new Blob([typeof src === "function" ? src.toString().replace(/^.+?\{/s, "").replace(/\}$/s, "") : src]));
super(ctx);
this.#ctx = ctx;
}
terminate(){
super.terminate();
URL.revokeObjectURL(this.#ctx);
}
}
まず、コンストラクタ引数としてメソッドかJavaScript文字列を受け取ります。
引数がメソッドだった場合はFunction.prototype.toString()
で文字列へ変換し、最初のfunction xxx(){
までと末尾の}
を正規表現で削ると、中身のソースコードだけが残ります。
引数がJavaScript文字列だった場合は何も加工しません。
そしてソースコードをnew Blob()
でファイルオブジェクト化します。
最後に、ソースファイルをBlobURLとして親クラスであるWorker
コンストラクタに渡します。
BlobURLは不要となったら明示的に解放することが望ましいので、ワーカースレッドを停止したら解放するようにterminate()
をオーバーライドします。
// function runByWorker(){ <= 1行目1文字目から"{"まで最短マッチで削られる
// 残るのは中身だけ
self.addEventListener("message", ({data})=>{
self.postMessage(`Worker Received: ${data}`);
});
// } <= 末尾の"}"が削られる
あとは、拡張クラスにメソッドを代入してあげるだけです。
const worker = new WorkerInline(runByWorker);
なお、引数に直接メソッドを書いても大丈夫ですし、アロー関数を使用した場合でも、マッチするのは最初の{
までと末尾}
に変わりはないので、問題ありません。
const worker = createWorker(()=>{
console.log("foo");
});
サンプル
class WorkerInline extends Worker{
#ctx = "";
constructor(src){
if(!(typeof src === "function" || typeof src === "string")){
throw new Error("argument must be 'function' or 'string'");
}
const ctx = URL.createObjectURL(new Blob([typeof src === "function" ? src.toString().replace(/^.+?\{/s, "").replace(/\}$/s, "") : src]));
super(ctx);
this.#ctx = ctx;
}
terminate(){
super.terminate();
URL.revokeObjectURL(this.#ctx);
}
}
const worker = new WorkerInline(()=>{
self.addEventListener("message", ({data})=>{
for(let i = 0; i < data.length; i++){
data[i] = Math.random() * 1000;
}
self.postMessage(data, [data.buffer]);
});
});
worker.addEventListener("message", ({data})=>{
worker.terminate();
console.log(data);
});
const bytes = new Uint8Array(1024 * 1024 * 128);
worker.postMessage(bytes, [bytes.buffer]);