現在JavaScriptの設定が無効になっています。このページを利用するにはJavaScriptの設定を有効にしてください。トップ全記事一覧テクノロジー2013.02.04JavaScriptとコールバック地獄Yahoo!デベロッパーネットワークの中野(@Hiraku)です。JavaScriptでサンプルコードを書く機会があったので、どんなインターフェースで提供するのが便利なのか考えてみました。問題になるコールバックのネスト問題について、一般的な話をまとめてみます。次のような処理を行う必要があるとします。「0」を出力する1秒待つ「1」を出力する1秒待つ「2」を出力するこれをプログラムで書くとどうなるでしょうか?シェルスクリプトの場合(同期)たとえばシェルスクリプトで素直に書くと、次のようになります。2お題をそのまま翻訳したようなプログラムになりました。変数もループも条件分岐も出てこない単純な見た目ですね。JavaScriptの場合(非同期)これがJavaScriptの場合、そのまま素直に書いていくと、次のようになります。出力にはconsole.log()を使うものとします。1000);ずいぶん見た目が変わりました。これに加えて「3,4,5以降も同じように出力」しようとすれば、さらに関数が入れ子になっていきます。この2つの例を比べると大抵の人は「シェルスクリプトの方が読みやすい」と思うのではないでしょうか。このように何の工夫もせずに書くと、関数の入れ子が積み重なっていき、コードがどんどん読みにくくなっていきます。コールバック地獄になるのかJavaScriptにはシェルスクリプトで言うところのsleepに相当する関数がありません。無限ループを使って無理やり再現することはできますが、もしそうしてしまうとsleep中はブラウザーがフリーズし、操作できなくなってしまいます。常にsetTimeout()のような関数を使い、非同期に処理を行っていくことが強く推奨されます。「処理が終わったら実行する関数」をあらかじめ渡しておく処理が終わったらその関数を呼び出すというインターフェースを持ちます。});非同期の関数を使う場合、処理前と処理後で関数を分割しなければなりません。これはどんなに同期的に(先ほどのシェルスクリプトの例のように)書きたくても、以下のような関数の羅列ができてしまうことを意味します。}];//これを何とかまとめて同期っぽくするしかないこの特徴により、非同期処理を綺麗に書くのは難しいのです。非同期は伝播するまた、非同期処理をまとめて同期的な見た目の関数を作ることはできません。一度でも非同期の関数を使ってしまうと、その処理全体もまた非同期な処理になってしまいます。JavaScriptを扱う上ではコールバック地獄は避けて通れず、JavaScriptを扱うエンジニアは日夜読みにくく乱雑化するコードと戦っています。この中で、数多くの解決策が編み出されてきました。GeneratorまずJavaScriptの次期バージョンで取り込まれるであろうGeneratorという機能を使った解決策を考えます。JavaScriptの将来の姿を見れば、きっとどのような解決策が理想的かわかるはずです。http://wiki.ecmascript.org/doku.php?id=harmony:generatorsGeneratorとは、端的に言うと関数の一時停止、再開ができる機能です。通常の関数は常に最初から実行され、returnが発生するまで一気に処理されますが、Generatorではその処理の途中で一度関数を抜けたり、また一時停止中の位置から処理を再開したりできます。一時停止、再開する際はパラメータの受け渡しも可能です。Firefoxが先行実装している例で説明します。yieldと書かれた部分が一時停止・再開のポイントになります。yieldまで到達すると処理は中断され、.next()もしくは.send()が呼び出されると、その時点から次のyieldに到達するまで処理されます。宣言しなければならないなど)がありますのでご注意ください。//2が出力、StopIteration例外が発生この機能を使うと、コールバック地獄を同期的なコードに修正できます。先ほど述べた通り、通常の非同期関数では「処理が終わったら実行する関数」をあらかじめ渡しておく処理が終わったらその関数を呼び出すというインターフェースを持ちますが、これを「処理が終わったら再開するGenerator」をあらかじめ渡しておく非同期関数を実行すると同時にGeneratorの処理を一時中断する処理が終わったらそのGeneratorを再開するという形に書き換えればよいのです。console.log(2);})();thread.next();入れ子状態になっていた関数が平準化されました。これだとまだ読みにくいですね。setTimeoutの部分を関数化してみましょう。yieldは引数を取ることができるため、yieldと関数の呼び出しは一行にまとめられます。console.log(2);})();thread.next();sleepという関数を定義した部分を除けば、かなり冒頭のシェルスクリプトの例に近づけることができたのではないでしょうか。これを順番ではなく、並列実行に書き換えるのも簡単です。先に非同期処理を並べておき、直後でyieldを必要な回数だけ並べておくだけです。console.log(2);})();thread.next();ただし、並列実行した処理のそれぞれの戻り値を受け取りたい場合は、もう少し工夫する必要があります。以上のGeneratorの使い方は一例であり、他にもさまざまな書き方が考えられます。再利用性の高い書き方を考えてみると面白いと思います。Generatorは「簡易的にイテレータを生成する機能」と説明されることがありますが、実は「本来複数の関数を作らないとできないかったことを、ひとつの関数にまとめてしまえる」という性質から、非常に応用範囲の広い機能です。多くの環境で使えるようになれば、JavaScriptのコードはかなり様変わりするかもしれませんね。jQuery.DeferredGeneratorによる解は分かりやすく理想的ですが、残念ながら今すぐ使える環境は非常に限られています。現状では、関数の羅列をうまく組み合わせるような補助ライブラリを使うことで解決するしかありません。コールバック地獄に対処するライブラリは数多くありますが、ここではメジャーだと思われるjQuery.Deferredの例を取り上げてみます。現状のJavaScriptでの対処を行うため、Generatorほどの劇的な効果はありませんが、コールバック地獄状態のコードを読みやすく改善できます。バージョンはjQuery1.9での動作を想定して解説します。jQuery.Deferredでは、それぞれの非同期処理をDeferredというオブジェクトでラッピングします。Deferredは「処理中か、処理が正常終了したか、エラーが起きたか」という状態を持っており、状態が変わったタイミングでそのイベントに対応する処理を呼び出します。Deferredオブジェクトは、入れ子にしたり、直列に連結したりして複雑な構造を持つことができ、どこか一部のDeferredオブジェクトの状態が変化すると、その状態が伝播していくことで処理が進みます。非同期関数を「Deferred化」します。関数の中でDeferredオブジェクトを作り、非同期処理が終わればDeferredオブジェクトの.resolve()を呼び出すようにセットしておき、最後に.promise()したものを返します。setTimeout()をDeferredに対応させると、以下のようになります。d.promise();}.promise()という謎のメソッドが書かれていますが、これはDeferredオブジェクトから.resolve()や.reject()といった「状態を変更するメソッド」を取り除いたサブセットを返します。Deferredオブジェクトを直接返してしまうと、受け取った側で状態を変更できてしまい、非同期処理そのものが本当に終わったのかどうか保証が取れなくなります。これを防ぐため、.promise()によるクリーニング作業が必要になるわけです。このsleep()を使うと、冒頭のお題は以下のように書き換えられます。});});...まだ入れ子が解消していないですね。工夫すると、.then()の位置を変えられます。console.log(2);});このように、.then()に渡した関数がDeferredを返す場合、.then()を連結する形に書き換えることができるのです。これで関数の入れ子を解消することができました。通常の用途ならここまでの改善でコードを書いていくことになります。sleep(1000)がまだ入れ子の内側に残ってしまっていますね。冒頭のシェルスクリプトの例と一対一になっていないように見えます。sleep()を関数で囲めば、.then()に渡すことができるようになります。});ここまで修正すればどうでしょうか。functionは消せないので冗長ではありますが、構造的には冒頭のシェルスクリプトの例と対応する形になったかと思います。ライブラリを使うべきかどうかjQuery.Deferredは優れた解決策ですが、一度使ってしまうと処理の根底からjQueryに依存してしまうという問題があります。非同期の処理を全てDeferred化しておかないと使えない(使いにくい)ためです。...いや、元からjQueryを使って開発している場合や、jQueryプラグインを作っている場合は何の問題もありません。Libraryなど他の系統のライブラリを使って開発している場合にも、Deferredに引きずられてjQueryが必要になってしまいます。依存のない、非同期処理専用のライブラリを使うことも可能ですが、すると今度はjQueryとインターフェースが合わず、jQueryと組み合わせる際の使い勝手が悪くなってしまいます。結局、昔ながらの「処理が終わったら引数から渡してもらった関数を実行する」というコールバックスタイルで作っておいた方が、汎用性は高くなります。どのライブラリでも、コールバックスタイルの非同期処理であれば、少しの修正でライブラリの流儀に合わせられるようになっているはずです。「あるライブラリ用に特化したプログラム」を書きたいのであれば、その作法に従うべきでしょう。「どんなライブラリと併用しても汎用的に使えるプログラム」を書きたいのであれば、外部に公開するインターフェースとしてはまずコールバックスタイルにしておき、必要に応じてライブラリに合わせたラッパーを作るという形になるかと思います。これは非同期処理に限った話ではありませんが、ライブラリを使うなら意識しておくべき点だと思います。非同期処理でよく問題になる点としては、他にもエラー処理があります。こちらはまた別の機会に書いてみたいと思います。JAPANでは情報技術を駆使して人々や社会の課題を一緒に解決していける方を募集しています。詳しくは採用情報をご覧ください。JAPANのサービス開発を支える技術やデザイン、文化を発信しています。Reserved.このページの先頭へ。