LoginSignup
7
7

More than 3 years have passed since last update.

WebWorkerのソースコードをインラインで完結させる

Last updated at Posted at 2019-01-16

WebWorker、使ってますか?

「JavaScriptはシングルスレッド処理だから」なんてもう言わせない。
JavaScriptでマルチスレッド処理を実現する素晴らしい機能です。

データ処理をワーカースレッドへ回すことにより、メインスレッドでのDOM描画をブロッキングしないので、重い処理を実装するときは、とても重要な機能となります。
実行エンジン側でスレッド管理をしてくれるので、スレッドセーフについても気にする必要はありません。

そんな良いことだらけのWebWorkerですが、見方によってはひとつ弱点があります。

WebWorkerを使用するためのWorkerコンストラクタには.jsファイルへの "パス" が必要なのです。
そう、ワーカースレッドで実行されるコードは、別ファイルとして用意しなければなりません。

しかし、プロトタイピングなどちょっとした用事で都度ファイルを用意するのは、とても面倒です。
そこで、WebWorkerをインラインで気軽にサクッと書いて使える方法を考えてみました。

解決策

まず、ワーカースレッドで実行したいコードをメソッドとして普通に書きます。
普通にメソッドを書いてるだけなので、エディタのシンタックスハイライトやサジェストも問題なく使えます。

Worker
function runByWorker(){
    self.addEventListener("message", ({data})=>{
        self.postMessage(`Worker Received: ${data}`);
    });
}

なお、このコードはあくまでワーカースレッドで実行されるので、DOM操作は出来ません。

次に、メインスレッドでWorkerコンストラクタへJavaScriptコードを直接代入できるWebWorkerの拡張クラスを作ります。

Main
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()をオーバーライドします。

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

SpecialThanks!

7
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
7