パスワードを忘れた? アカウント作成
201347 journal

Torisugariの日記: JavaScriptの非同期コールバック

日記 by Torisugari

例えば、img要素にウェブ上の画像を読み込んで、完了後に何らかの処理を行う必要があったとします。その際、

  1. var image = document.createElement("img");
    image.src = "http://www.example.com/foo.png";
    image.onload = function() {alert("Loading has finished!");};

  2. var image = document.createElement("img");
    image.onload = function() {alert("Loading has finished!");};
    image.src = "http://www.example.com/foo.png";

の両者を比べて、どちらが正しいと思いますか?

蛇足ですが、2行目と3行目が入れ替わっているだけです。加えて補足すると、1行目はあまり重要ではないので、createElementの代わりにgetElementById等で既存の要素を操作してもこの議論は成立ちますが、描画がUIと同じスレッドだと話がややこしくなるので、純粋にJavaScript(+DOM)エンジンの中だけで描画が完了するcreateElementで考えてください。

私の好みからいくと、断然、後者を推しますが、答えは「どちらでもよい」ということになるでしょう。実際のところ、「1.」の方を例文として採用している場合にしばしば遭遇します。「1.」をスクリプトだけで解釈すると、まさに、泥棒を追い込んだ後で縄を綯うようなものですから、気持ち悪いというか、落ち着かないこと甚だしいロジックです。しかし、「1.」のコードでも間違いなくalertが呼ばれます。

なぜ、なのでしょうか?通常の場合、「1.」はレースコンディションと判断されてもおかしくないように思います。超絶的な性能の回線を通ってfoo.pngがダウンロードされたのに、プロセッサが遅すぎてonloadへの代入が済んでいないケースだってありそうなものです。まあ、そこまで言うと自分でも無理がある気がしますが、100%とは言えない、というのが常識的な判断でしょう。

このままでは水掛け論なので、「1.」に1行追加した次のようなスクリプトを考えてみます。

var image = document.createElement("img");
image.src = "http://www.example.com/foo.png";
for (var i = 0; i < 1000000000; i++);
image.onload = function() {alert("Loading has finished!");};

これならどうでしょうか?

for (var i = 0; i < 1000000000; i++);

は、単に1,000,000,000回ループをまわすだけのアルゴリズム(というのは大げさか)です。JavaScriptエンジンが理想的な挙動をしても、「足し算+比較」の合計が1clock以下ということはないでしょうから、10GHzのプロセッサでも、最低0.1秒はこのスレッドが足踏みします。実際にはもっとかかるでしょう。これは、img要素が読み込みを完了して、loadイベントを放出するまでの時間といい勝負ができる数値です。画像がキャッシュにある場合はloadイベントの方が遥かに早く発生します。

しかしながら、この場合でもやはりalertは実行されます。つまり、別スレッドで発生したイベントは、スクリプトが実行中である限り、そのスレッド(=UIのスレッド)に戻って来られない(割り込めない)のです。したがって、イベントハンドラの呼び出しは、必ずイベントハンドラの設定後になり、泥棒に縄が間に合います。

ただし、これは私の知る限り、明文化された仕様ではありません。単に各々のブラウザがこのように実装されている、というだけです。実はIE8では「1.」は動かないので、なるべくなら「2.」を使うのが望ましいでしょう。

と、ここで終われば結構な話ですが、そもそも、こんなショボイ結論を出すためにこの話を始めたわけではありません。予備知識はこのくらいにしておいて、ここからが本題です。

whatwg/w3cではWeb WorkersWeb Socketsが検討されており、いくつかの先行実装が既に存在します。詳しく説明すると長くなるので、詳細は割愛しますが、これらのクラスは、コンストラクタでスレッドが(Web Socketsは一時的に)分岐します。つまり、「2.」のような書き方はできません。必ず「1.」のように、走り出した後にイベントハンドラを登録しなければならないのです。

さて、一体どうすれば良いのでしょうか?先に

つまり、別スレッドで発生したイベントは、スクリプトが実行中である限り、そのスレッド(=UIのスレッド)に戻って来られない

と言いましたが、あくまでこれは実装に依存した話で、仕様で説明できるような立派なテクニックではありません。しかも、逆に言うと、スクリプトが実行中でなければ、この絆創膏は使えないわけですから、抜け道もあります。

というわけで、PoCを書いてみました。Web Workers対応のブラウザで試してみてください。
http://torisugari.hostei.com/development/webworker/worker.js
http://torisugari.hostei.com/development/webworker/worker.html
この例では、理想的な挙動をすると、リンクをクリック後にworker.jsが発したメッセージが書き込まれます。先に説明したように、kickのようなスクリプトは問題なく動きますが、punchの方はレースコンディションを勝ち抜けずに失敗してしまうのです。

まあ、失敗する方が当たり前、というか、納得のいく挙動だと思いますが、このテクニックに頼り切ってしまうか、正しく書くか、あなたならどちらを選びますか?

ちなみに、正しくすると、おそらく次のようになり、きちんとpunchも動作します。
http://torisugari.hostei.com/development/webworker/worker2.js
http://torisugari.hostei.com/development/webworker/worker2.html

ここで、worker側のonmessageのパースがUI側からのpostMessageの割り込みより遅い実装が存在したら、などと考え出すと眠れなくなりそうですが、そこはProcessing modelで保障されていますから、大丈夫です。

しかし、正義の常として、worker.jsとworker2.jsを比べてみると、どちらが簡単かは一目瞭然なので、なかなか悩むところではないですかね。チュートリアルが既にこれですし。

この議論は賞味期限が切れたので、アーカイブ化されています。 新たにコメントを付けることはできません。
typodupeerror

あと、僕は馬鹿なことをするのは嫌いですよ (わざとやるとき以外は)。-- Larry Wall

読み込み中...