約束の地 – Promised land

この記事は、 Sencha Advent Calendar 2013 の12日目の記事です。

都市再開発で、オフィスを市のやや北寄りの区域に移さなければならなくなった。 新しいオフィスは、マサチュセッツ大通りとボイルストン通りの角に突っ立っている二階建ての丸い塔のような建物の二階にあって、一階は煙草屋である。 前の住人は女占い師で、彼女が看板がわりに窓に貼りつけたつぎはぎの金文字をかみそりの刃で削りとっている時、その男の姿に気がついた。

ロバート・B・パーカー著「約束の地」より。

パーカーも亡くなっちゃったんですね。

今回はPromiseについてお話しします。

Senchaのフレームワークでは、多くの場面で非同期の処理をする必要があります。 その最たるものがサーバーとの通信です。

[js] Ext.Ajax.request({ url: ‘page.php’, params: { id: 1 }, success: function(response){ var text = response.responseText; // process server response here } }); // next step [/js]

failureがありませんね。ごめんなさい。m(_ _)m failureちゃんと書きましょう。 リクエストの結果は、非同期で実行されるsuccess関数内で受け取ります。 受け取った結果をもとに次の処理をするときには、当然ながらsuccess関数内から実行しなくてはなりません。 コード中の process server response here という所で実行するわけですね。 コード上の next step と書かれた場所で何かを実行しても、サーバーからのレスポンスはこの時点では通常は返ってきていません。

複数の非同期処理をチェーンの様に実行したい時ってあります。 そんな時は、こうして関数の中にネストして処理してやると、どんどんネストが深くなって悲しいことになります。

[js] Ext.Ajax.request({ params: { id: 1 }, success: function(response){ var text = response.responseText; Ext.Ajax.request({ url: ‘page2.php’, params: { text: text }, success: function(response){ var text = response.responseText; Ext.Ajax.request({ url: ‘page3.php’, params: { text: text }, success: function(response){ var text = response.responseText; // process server response here } }); } }); } }); [/js]

そこで、非同期の処理をシーケンシャルな処理のように書く方法ってないものかなぁ、と思うわけです。 こういう要求にはいろいろなライブラリとかあるんですよね。

CommonJSのPromiseパターンというのがあって、それがどうもこのやり方のこれからの標準のようですね。

Senchaのブログ記事、 Implementing User Extensions for Sencha Architect 3.0 Preview (日本語訳は「 Sencha Architect 3.0 プレビューでのユーザー拡張機能の実装」) では、 深くネストした関数コールバックの解決に、promiseを使うことにした、ということが記述されています。

なんかいいライブラリないかな?

私は、みつけました。

_人人人人人人人人_
> Ext.ux.Deferred <
 ̄Y^Y^Y^Y^Y^Y^ ̄

Github – wilk/Ext.ux.Deferred

今日は、このクラスの使い方を勉強してみましょう。

名簿のアプリを例にします。

er

このように、personsテーブルでは、prefectures(都道府県)、carriers(携帯キャリア)、companies(会社)という3つのマスターを参照しています。これを

スクリーンショット

このようにGridの中に表示したいとします。

それぞれのテーブルは、Persons/Prefectures/Carriers/Companies というストアに格納され、それぞれがAjaxプロキシでもってサーバーからデータを取得します。

グリッドでは、次のように’renderer’関数を使って表示してやります。

[js] xtype: ‘grid’, store: ‘Persons’, columns: [{ text: ‘名前’, dataIndex: ‘name’, flex: 1 }, { text: ‘都道府県’, dataIndex: ‘pref_id’, renderer: function(value) { var store = Ext.getStore(‘Prefectures’), rec = store.getById(value); return rec.get(‘pref_name’); }, flex: 1 [/js]

4つのストアは、コントローラーのonLaunchメソッドでロードしましょう。

[js] onLaunch2: function() { var me = this; me.getPrefecturesStore().load(); me.getCarriersStore().load(); me.getCompaniesStore().load(); me.getPersonsStore().load(); }, [/js]

Personsストアをロードする前にちゃんと他のマスターをロードしてますね。これで大丈夫です。

って、そんなわけないだろ!(`Д´)ノ

こちら、loadメソッドは非同期ですので、マスターのロードが終わっていなくても、Personsストアのロードが始まってしまう可能性があります。 ためしに、マスターのサーバー側メソッドでWaitをかまして、遅くしてやると。

スクリーンショット

このようにエラーが発生して、カラムにデータが表示されません。 これは、マスターがロードし終わる前にPersonsストアがロードされてしまって、Gridのrendererが呼ばれてしまっているからです。

これを Ext.ux.Deferredですべてのマスターのストアがロードされてから、Personsストアをロードするようにしてみましょう。

まず、Ext.ux.Deferredをプロジェクトに追加します。 僕の場合は、 app/ux/Deferred.js に置きました。 Ext.uxというネームスペースなので、そのままだとSDKのソースディレクトリに取りに行ってエラーになるので、app.jsで、Ext.Loaderにパスを教えます。 app以外のところ(classpathが通っていないところ)においた場合は、app.jsonとかsencha.cfgとかでclasspathを設定してやる必要があります。

[js] Ext.Loader.setConfig({ paths: { ‘Ext.ux.Deferred’: ‘app/ux/Deferred.js’ } }); [/js]

そして、これを使う、コントローラーの requires に指定してロードさせます。

[js] requires: [ ‘Ext.ux.Deferred’ ], [/js]

Ext.ux.Deferredの使い方は、

[js] Ext.ux.Deferred.when( promise ).then(resolve[, reject]); [/js]

こんな風に呼び出します。promiseは、非同期処理をする関数です。resolveはpromiseの非同期処理が完了したら呼び出される関数。rejectは非同期処理が却下されたときに呼び出される関数です。 thenの後に更に、.thenとして処理をチェーンすることもできます。

promiseにセットする関数は、Ext.ux.Deferredのインスタンスを返す関数である必要があります。

async = function() {
    var dfd = Ext.create('Ext.ux.Deferred');
    asyncFunc(function() {
        dfd.resolve();
    });
}

このような感じになります。 関数の中でExt.ux.Deferredのインスタンスを生成して、非同期処理が終わったらそのインスタンスのresolveメソッドを呼んでやります。 それをExt.ux.Deferredのスタティックメソッドであるwhenに渡してやります。

[js] Ext.ux.Deferred.when( async ).then(function () { // 終わった後の処理 : }); [/js]

また、whenメソッドに渡すpromise関数は複数指定できます。

Ext.ux.Deferred.when( promise1, promise2, promise3 ).then(resolve[, reject]);

この方法で、3つのマスターをロードして、それらがすべて整ったらPersonsストアをロードするというのを実現できそうです。

ストアをオープンするためのpromise関数は次のような感じになります。

[js] promise = function() { var me = this, dfd = Ext.create(‘Ext.ux.Deferred’); me.getPrefecturesStore().load({ callback: function(rec, op, success) { if( success ) { dfd.resolve(); } else { dfd.reject(arguments); } } }); return dfd; }; [/js]

Ext.ux.Deferredオブジェクトを生成して、loadメソッドのコールバックの中で、resolveまたはrejectを呼び出しています。

このやり方でマスター全部のpromise関数を作ってやります。 出来上がったonLaunchメソッドは次のようになりました。

[js] onLaunch: function() { var me = this, promise = function(store) { return function() { var dfd = Ext.create(‘Ext.ux.Deferred’); store.load({ callback: function(rec, op, success) { if( success ) { dfd.resolve(); } else { dfd.reject(arguments); } } }); return dfd; }; }; Ext.ux.Deferred .when( promise(me.getPrefecturesStore()), promise(me.getCarriersStore()), promise(me.getCompaniesStore()) ) .then( function(){ // when resolved me.getPersonsStore().load(); }, function(){ // when rejected console.log(arguments); } ); } [/js]

3行目からのpromiseという関数は、指定されたストアをロードするpromise関数を返します。 この関数を使ってpuromise関数を3つつくってwhenメソッドに渡しています。 こうすることで、全てのマスターストアがロードされてから、Personsテーブルをロードすることを確実にすることができました。

Ext.ux.Deferredは、この他にもExt.Directでのサーバーサイド関数呼び出しでのチェーンなんかにも使えますし、コンポーネントテストを書くときに使っても便利ですね。 Senchaフレームワークの標準で、非同期の実行を待つ処理を書けるのは、たしかSencha TouchのRouter機能のbeforeフィルターの中だけだったと思います。 beforeフィルターのように、ある処理をする前に、なにかがどうにかなっているのを確実にするために、このExt.ux.Deferredを使うと、より安定したアプリケーションを構築できる可能性がありますね。

4 thoughts on “約束の地 – Promised land

  1. ピンバック: 非同期のJavaScript: Promise | Sunvisor Lab. Ext JS 別館

  2. pianyi

    こんにちは 困ったらここにたどり着く事が多く、いつも重宝しております。 1か所間違いでは?と思う個所が有りましたのでご連絡いたします。ご確認いただけないでしょうか。

    一番最後に記載されているソースの16行目: 「return dfd;」 の部分ですが、 「return dfd.promise();」 の間違いでは無いでしょうか。

    GithubのTutorial では、「return dfd.promise();」が記載されていると思われます。

    よろしくお願いいたします。

    Ext4.1.3 chrome : 56.0.2924.87 (64-bit)

  3. Sunvisor

    コメントありがとうございます。 この記事の執筆時点では return dfd で正しかったのが、ライブラリの方のバージョンアップで仕様が変わったのだと思います。最新の仕様に合わせて記述してください。

  4. pianyi

    ご回答ありがとうございます。

    履歴の確認を怠っておりました。 失礼いたしました。

コメントは停止中です。