async:false とは何か。或いは、非同期処理を諦めるのはまだ早い!


  • JavaScript は非同期処理が基本
  • jQuery は非同期処理を簡単にしてくれる
  • Knockout MVVM における非同期処理パターン

の三本立てでお送り致します。

JavaScript は非同期処理が基本

最近、こんな記事をいくつか見かけました。

「jQuery.ajax で結果が反映されない!困ったら async:false だ!」

これは Tips とは言えません。 async:false にすると何が変わるのか、正しく認識していますか?
jQuery の API リファレンス には、こうあります。

Note that synchronous requests may temporarily lock the browser, disabling any actions while the request is active.
同期リクエストは 一時的にブラウザをロックし、一切のアクションを受け付けなくなる ことに注意してください。

JavaScript のみならず、クライアントサイドの技術は非同期処理を基本とした世界がスタンダードになってきました。
理由は簡単で、通信やちょっとした重い処理をする度にフリーズするソフトウェアなんてストレスが溜まるからです。
だからこそ、 jQuery.ajaxasync (=非同期)オプションはデフォルトで true に設定されているのです。

でも結果が反映されないよ。そもそも非同期ってなんなん?

例えば、引数を渡して結果が返却されるシンプルな関数は同期処理です。

同期処理
function work_sync(param) {
    // なにか時間がかかる処理をやる
    return "result" + param;
}

var result = work_sync('nanika'); // ここでブロックされ、処理が終わったら次の行へ
alert(result);                 // 「resultnanika」

work_sync() を呼び出すと、処理が行われて結果が var result に格納されます。
たとえばここで work_sync の中で5秒かかる処理が行われた場合、ブラウザは5秒間フリーズします。
Chrome ならマルチプロセスなのでタブ切り替えくらいならできますが、その他のブラウザではタブ切り替えもできなくなるかもしれません。

それではこれを非同期にしてみます。

非同期処理
function work_async(param) {
    setTimeout(function() {
        // 時間がかかる処理をする
        return "result"+param;
    }, 0);
}

var result = work_async('nanika');  // ブロックはされず、すぐに次の処理へ移行する
alert(result);            // 「undefined」

簡易に非同期処理させるために setTimeout を使ってみました。
setTimeout は第二引数に指定されたミリ秒が経過したタイミングで処理を実行しますが、
0ミリ秒と指定することで「今すぐ、ただし現在の処理フローとは別に実行」することができます。
その中でどれだけ時間がかかろうと、 work_async 呼び出しではブロックされずブラウザもフリーズしません。

ただし、結果は返ってこないため undefined と表示されてしまいます。
setTimeout に渡した関数の返却値をだれも受け取っていないので、当然の結果です。
これが、結果が反映されずに困る原因です。

コールバック

先ほどの例を解決する第一の方法として コールバック があります。

function work_async(param, callback) {
    setTimeout(function() {
        // 時間がかかる処理をする
        callback("result"+param);
    }, 0);
}

var callback = function(result) {// 処理結果を引数としてうけとる
    alert(result);
}
work_async('nanika', callback);  // 引数として関数を渡す

これで work_async の処理が 終わったタイミングで alert が表示されます。
下記のような jQuery.ajax の呼び出しも、基本的にコールバックを使っていました。

var items;
$.ajax({
    url:"/foo/bar",
    type:"get",
    dataType:"json",
    success: function(response){  // ←これもコールバック
        if (response.success) {
            items = response.items;
        }
    }
});
alert(items.length);   // ←?

最後の alert ではエラーが発生します。
重要なのは、非同期処理の場合 一直線だった処理フローが分岐する ということです。
JavaScript の場合、一度分岐した処理はもう戻ってきません。

エラーが起きる理由は処理の順番を考えればわかります。

  1. $.ajax を呼び出す。通信は現在のフローでは行わないのですぐに完了。
  2. alert(items.length) を呼び出す(まだ空っぽ)。
  3. 通信が終わった後、success として渡した関数が実行されて item に結果が反映される。

もちろん、 async:false を指定すればエラーはなくなります。
その理由は、ブラウザをフリーズさせる代わりに処理の分岐をさせずに済むからです。

jQuery は非同期処理を簡単にしてくれる

上記の例は、こんなふうに書くことができます。

$.ajax({
    url:"/foo/bar",
    type:"get",
    dataType:"json"
}).then(function(response) {
    if (response.success) {
        alert(response.items.length);
    }
});

これは jQuery.Deferred という非同期処理を簡単に扱うための機能です。
then の部分が Deferred で、非同期処理を少し同期処理っぽく書くことができます。
jQuery.ajax を使う場合は、最近はこちらが標準と言えます。
jQuery プラグインじゃありませんよ!jQuery 標準機能です!

jQuery.Deferred については詳細な記事がたくさんありますので、こちらでは解説いたしません。
爆速でわかるjQuery.Deferred超入門
結局jQuery.Deferredの何が嬉しいのか分からない、という人向けの小話

Knockout MVVM における非同期処理パターン

Knockout という JS フレームワークがあります。JS 上で MVVM という設計手法を実現するためのフレームワークなのですが、実は非同期処理と仲良くできるフレームワークでもあります。

Knockout 最大の特徴は「データバインディング」で、 View (HTML) と ViewModel (JSオブジェクト) をひもづけると、相互に自動反映してくれます。そしてその機能のほとんどを支えているのが、 Observer パターン です。

View
<span data-bind="visible: isLoading">読み込み中...</span>
<ul data-bind="foreach: members">
    <li>
        FirstName: <input type="text" data-bind="value: firstName"/>
        LastName: <input type="text" data-bind="value: lastName"/>
        Email: <input type="text" data-bind="value: email"/>
    </li>
</ul>
ViewModels
function AppViewModel() {
    var self = this;

    // fields
    self.members = ko.observableArray([]);   // メンバー配列
    self.isLoading = ko.observable(false);

    // methods
    self.reload = function() {                 // リロードメソッド
        self.isLoading(true);
        $.ajax({
            url: "/foo/bar",
            type: 'get',
            dataType: 'json'
        })
        .then(function(response) {
            if (response.result != 'success') {
                alert(response.error);
                return;
            }
            self.members([]);
            for (var i in response.members) {
                self.members.push(new MemberViewModel(response.members[i]));
            }
        }, function() {
            alert('通信エラー');
        })
        .always(function() {
            self.isLoading(false);
        });
    }

    self.reload();
}

function MemberViewModel(data) {
    var self = this;

    data = $.extend({}, {
        firstName: "",
        lastName: "",
        email: ""
    }, data);

    self.firstName = ko.observable(data.firstName);
    self.lastName = ko.observable(data.lastName);
    self.email = ko.observable(data.email);
}

$(function() {
    ko.applyBindings(new AppViewModel());  // バインド実行
});

上の例で重要なのは、 メンバー配列 AppViewModel.members とリロードメソッド AppViewModel.reload です。
リロードメソッドはアイテムをサーバから取ってきた後、メンバー配列に上書きします。

「ViewModel は非同期処理を行った後、自身のプロパティを書き換える」 ということしかしていませんが、これだけで View に反映されますし、アプリケーションとして成立しています。

これで成立する大きな要因が Observer パターン です。
メンバー配列プロパティは ko.observableArray として宣言(生成)されており、これは書き換わると同時にフレームワークへ変更が通知される仕組みを持っています。

Knockout はバインディングエンジンですが、observable をうまく扱うことでシンプルなコーディングができるようになります。
(余談ですが、私はバインドではなく observable が使いたいがために Knockout を用いることもあります)

追記:2014/1/15
MVVM (Model-View-ViewModel) という設計概念、機能の切り分け方について詳しく学びたい、という方は スライド:MVVMパターンとは をご覧になるのが一番の近道だと思います。

まとめ

JS は基本、非同期処理です。よほどな要件が無い限り、非同期処理を捨てるべきではありません。
フロントエンドエンジニアにはならない、というのであればその限りではありませんが、そうじゃないならば非同期処理とできるかぎり仲良くやっていきたいものです。