2019年8月13日火曜日

Web Worker の使い方ざっくりまとめ

Web Worker は全然使ったことなかったのですが、 使っているライブラリを触って苦労したので、最低限の使い方をまとめます。 まずは以下のコードが基本。これは main.js から worker.js を Web Worker として呼び出す例です。 呼び出したときの処理として load, proc の 2 つを定義しています。
// main.js
var worker = new Worker('worker.js');
worker.postMessage({type:'load'});
worker.addEventListener('message', function(event) {
  if (event.data.type == 'result') {
    document.getElementById('foo').value = event.data.a;
  }
});
worker.postMessage({type:'proc'});

// worker.js
self.addEventListener('message', function(event) {
  if (event.data.type == 'load') {
    // load
  } else if (event.data.type == 'proc') {
    event.data.type = 'result';
    event.data.a += 1;
    postMessage(event.data);
  }
});
worker.js 側では addEventListener で message イベントハンドラを Listen して通信を待ちます。 通信は postMessage 関数で行います。 処理は非同期に行われるため、結果を受け取って何かをするには、 worker.js 側で postMessage 関数を呼び出し、 main.js 側でも message イベントハンドラを Listen にすることで実現できます。


複数の処理を Web Worker で処理させる

だいたいの仕組みはこれで良いのですが、 複数のメッセージを送信し合ったり、複数の処理を行う場合、少し面倒です。 proc 関数の結果を受け取って、さらに proc1, proc2 を実行するにはどうすればいいでしょうか。 これは main.js を以下のように変えれば良さそうです。
// main.js
var worker = new Worker('worker.js');
worker.postMessage({type:'load'});
worker.addEventListener('message', function(event) {
  if (event.data.type == 'result') {
    if (event.data.id == 1) {
      document.getElementById('foo').value = event.data.a;
    } else {
      document.getElementById('bar').value = event.data.a;
    }
  }
});
worker.postMessage({type:'proc', id:1});
この例で注意しないといけないのは、main.js 側の message イベントハンドラを複数用意したとしても、 proc1, proc2 を実行する条件がわからないということです。 DOM ノードを渡すことなどもできないので、postMessage 関数を呼び出す際に、 id などの何らかの情報を付ける必要があります。

その時に重要なのが、worker.js をライブラリとして提供する場合、 worker.js の postMessage 関数を定形データにはできないということです。 定形データにしようとすると id を返り値として返却することができず、 どのノードに対して処理をするかわからなくなってしまいます。 どうしても定形データとして処理したい場合、worker.js 側で id に関する処理を記述しなければいけないと思います。 ここで少しハマりました。


大量の DOM ノードを Web Worker に投げる

もう1つの問題は、大量の DOM ノードを worker に投げて、その結果を DOM ノードに反映させる必要がある時です。 worker は DOMノードにアクセスできないので 情報を渡すことができません。 代わりに message イベントハンドラは外側のスコープのデータを見れるようです。 なので以下のように ID 渡しで書けます。
// main.js
var worker = new Worker('worker.js');
worker.postMessage({type:'load'});

var nodes = document.getElementById('hoge').children;
worker.addEventListener('message', function(event) {
  if (event.data.type == 'result') {
    if (event.data.id == 1) {
      nodes[i].value = event.data.a;
    } else {
      document.getElementById('bar').value = event.data.a;
    }
  }
});
for (var i=0; i<nodes.length; i++) {
  worker.postMessage({type:'proc', id:i});
}


結果をマージする → たぶんできない

結果をマージしたい場合にはどうすれば良いでしょうか。 …どうやらこれは無理な気がしています。 普通は以下のように Promise を使って終了判定すれば良いのですが、 Web Worker の場合は worker の postMessage の終了ではなく、 その結果を受信した後に処理したものすべての終了状態を知らないといけない。 でもそこにフックをかけるのは無理っぽい。 イベント駆動型だから仕方ないか。 最後にマージしないでも済むように、事前にノードを作ったりして、 非同期になるようにコードを書き換えるしかないのでしょう。
var procAll = new Promise(function() {
  for (var i=0; i<nodes.length; i++) {
    worker.postMessage({type:'proc', id:i});
  }
});
Promise.all(procAll).then(function(results) {
  :
});


はー。JavaScriptまったくわからない。

0 件のコメント: