0
0

More than 3 years have passed since last update.

WebWorkerで擬似チャネルインターフェイスを作りVue.jsのライフサイクルへバインドする

Posted at

Vue.jsを使ったSPAで、メインスレッドは極力Vue.jsのレンダリングに使わせたいので、重い処理はWebWorkerへ投げることがあります。

WebWorkerは非同期のためaddEventListener()などでメッセージをハンドリングするわけですが、ハンドラを普通に書くとVue.jsのライフサイクルから外れてしまいます。

強引にライフサイクル内でイベントリスナを登録してしまうと、多重登録となりやすく危険です。

<div id="vue">
    <input type="file" v-model="largeBlob">
    <div>{{result}}</div>
    <button @click="heavyTask()">Parse!</button>
</div>
const worker = new Worker("./worker.js");

// 受けるときはライフサイクル外になってしまう
worker.addEventListner("message", ({data}) => data);

const vue = new Vue({
    el: "#vue",
    data(){
        return {
            largeBlob: undefined,
            result: ""
        };
    },
    methods: {
        async heavyTask(){
            // heavyTaskが呼ばれるたびに多重登録されてしまう
            worker.addEventListner("message", ({data}) => this.result = data);

            // ライフサイクル内で送ることはできる
            const _largeBlob = await this.largeBlob?.arrayBuffer();
            worker.postMessage(_largeBlob, [_largeBlob]);
        },
    }
});

そこでWebWorkerの処理結果をmethods内のメソッドへバインドすることで、安全にVue.jsのライフサイクルへ組み込む方法をご紹介します。

擬似チャネルインターフェイス

まずDedicatedWorkersはチャネル名を指定できません。
ですが、実装していくうちコンテキストによって処理内容を変えたい場合が出てくると思います。
その答えのひとつとして "擬似チャネルインターフェイス" という方法を私は思いつきました。
なお、これはVue.jsに限った話ではなく、汎用的に使えるテクニックです。

まず、送受信メッセージをオブジェクトでカプセル化してchなどのチャネル名を指定するプロパティを作ります。
送受信メッセージの中身は、配列としてbodyなど名前を付けてプロパティへ格納すると、もし中身がArrayBufferだった場合はpostMessage()の第2引数のTransferableで指定しやすいのでオススメです。

MainThread
const message = {
    ch: "parse",
    body: [_largeBlob]
};
worker.postMessage(message, message.body);

いっぽう、ワーカースレッドはメインスレッドから送られてきたメッセージをチャネルごとに分けて処理する必要があります。

まずはfunction _channel_parse(data)など、チャネルインターフェイス用だと分かりやすい接頭辞とチャネル名を繋げたメソッドを作り、引数にはメインスレッドから受信したメッセージを受け取ります。

そして、ワーカースレッド側のメッセージイベントハンドラを組み立てるわけですが、先ほど作ったメソッドはグローバルスコープへ定義されているはずです。
具体的にいうとDedicatedWorkerGlobalScopeselfglobalThisから参照できます。

ここが大事で、JavaScriptはプロパティへアクセスする際にブラケット記法を使うことで、変数値でプロパティへアクセスすることができます。
つまりselfを親オブジェクトとして、そのプロパティ(つまり_channel_メソッド)へチャネル名でアクセスする、というのが擬似チャネルインターフェイスの特徴です。

チャネルメソッドの返り値は、メインスレッド同様にオブジェクトでカプセル化しチャネル名プロパティを付与します。

WorkerThread
async function blobParser(blob){
    // 何らかの重い処理
    return result;
}

async function _channel_parse(data){
    return {
        ch: "parse",
        body: [await blobParser(data)]
    };
}

self.addEventListner("message", ({data})=>{
    const message = await self[`_channel_${data.ch}`](data);
    self.postMessage(message, message.body);
});

そして再びメインスレッドへ戻ってきます。
ここでようやくVue.jsのライフサイクルへ組み込む処理となります。
といっても、やってることは先ほどのワーカースレッドとほとんど同じです。
先ほどの親オブジェクトはDedicatedWorkerGlobalScopeでしたが、Vue.jsのライフサイクルフックはthisで参照できるためselfthisに変えるだけです。

MainThread
new Vue({
    el: "#vue",
    data(){
        return {
            largeblob: undefined,
            result: ""
        };
    },
    mounted(){
        // チャネル名でmethodsへバインドする
        // マウント後に1回だけ登録されるので、多重登録は発生しない
        worker.addEventListener("message", ({data}) => this[`_channel_${data.ch}`](data));
    },
    methods: {
        // WebWorkerの処理結果メッセージを引数として受け取れる
        _channel_parse(data){
            this.result = data.body[0];
        },
        async heavyTask(){
            const message = {
                ch: "parse",
                body: [await this.largeBlob?.arrayBuffer()]
            };
            worker.postMessage(message, message.body);
        },
    }
});
0
0
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
0
0