[jQuery]はじめてdeferredを使って非同期処理を扱うときにつまりそうなところ

[jQuery]はじめてdeferredを使って非同期処理を扱うときにつまりそうなところ

目次

はじめに
非同期処理
deferredは非同期処理が終わるまで待つわけではない
終わり

はじめに

jQueryで非同期処理を扱う際によく聞くdeferredについて分かりはじめてきたので、勉強中の方へ少しでも手助けになればと思い書いてみました。
ただdeferredの使い方についてはいろいろな方が書いているので、今回はdeferredについて勉強する際に私がつまずいたところを書こうと思います。
また今回はdeferredの説明ではなく非同期処理が入っても処理の順番を理解しやすいように書いているので、正確な言い回しになっていませんがご了承ください。

非同期処理

プログラムは基本的にはソースコードに書かれた内容を上から順に実行していきます。
ですが、非同期処理という、必ずしも上から順番に実行するとは限らない処理があります。
この場合にも意図した順番で実行してほしという時にdeferredを使用する方が多いかと思います。

まずは下記のコードはサンプルとして使用します。
(メモ帳か何かに貼り付けて適当な名前で保存してください。
その際に拡張子を.htmlにして保存し、保存先は覚えておいてください。)

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>非同期処理サンプル</title>
    <script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
</head>

<body>
    <button type="button" id="startButton">処理開始</button>
</body>

<script type="text/javascript">
    $('#startButton').click(function (e) {

        console.log("1番目");

        console.log("2番目");

        setTimeout(function () {
            console.log("3番目");
        }, 1000);

        console.log("4番目");

        console.log("5番目");

    });
</script>

</html>

このページをブラウザで開いてF12を押して開発者ツールを開き、ページにあるボタンをクリックします。
そうすると下記のような結果になります。

出力結果を見ると明らかですが、3番目に出力されてほしいものが最後に出力されています。
これが非同期処理の特徴です。
今回の場合ですと、setTimeoutが順番を狂わせています。

setTimeoutは「特定の処理を、指定した時間が過ぎた後に実行する」というもので、2つの引数を使います。
1つ目の引数には特定の処理を渡し、2つ目の引数には指定した時間を渡します。
つまりsetTimeoutは「1つ目の引数に渡された内容を、2つ目の引数に渡された時間だけ待ってから実行する」というものになります。
ただ厄介なのが待っている間に後ろの処理を実行し続けていきます。
後ろの処理を意図した順番通りに実行させたいのでdeferredを使用します。
その際に私がつまずいたところも一緒に書いていきます。

deferredは非同期処理が終わるまで待つわけではない

まずdeferredを使用するためのコードを書きます。
といってもこのページを見られている方は恐らくdeferredを使ってみた経験があるかと思いますので
詳細については省きます。

<script type="text/javascript">
    $('#startButton').click(function (e) {

        console.log("1番目");

        console.log("2番目");

        // deferredの宣言
        var defer = $.Deferred();

        setTimeout(function () {
            console.log("3番目");
        }, 1000);

        // 宣言したdeferredからpromiseを取得
        var promise = defer.promise();

        console.log("4番目");

        console.log("5番目");

    });
</script>

次に非同期処理の中で非同期が終わったことを知らせるためにdeferredのresolveを実行します。

        setTimeout(function () {
            console.log("3番目");
            // 宣言したdeferredのresolveを実行
            defer.resolve();
        }, 1000);

これで一度保存して最初同様にブラウザを開いてボタンをクリックします。
その結果がこちらになります。

理解されている方からしたら当たり前ですが、 最初と結果が変わっていないですね。
この理由ですが、deferredを使ってpromiseを取得しましたが、promiseは非同期を待つというものではなく
「特定の処理を、指定したタイミングで実行する」というものになります。

これってsetTimeoutとすこし似ていると思いませんか?
setTimeoutの場合は1つ目の引数の内容を、2つ目の引数で指定した時間だけ待って実行するというものでした。
promiseの場合も同じで1つ目の引数の内容が特定の処理にあたります。
ただsetTimeoutと違い、promise(1つ目の引数)のように書くのでなく、以下のように書きます。

promise.then(function(){
     // deferredのresolveが実行された後に実行したい処理
});

次に実行されるタイミングについてですが、setTimeoutは2つ目の引数で指定した時間を待つというものでした。promiseの場合は2つ目の引数ではなく、promiseを取得するために使ったときのdeferredがresolveを実行したときになります。
ということは今回のコードを順番通りに実行される場合には下記のようになります。

<script type="text/javascript">
    $('#startButton').click(function (e) {

        console.log("1番目");

        console.log("2番目");

        // deferredの宣言
        var defer = $.Deferred();

        setTimeout(function () {
            console.log("3番目");
            // 宣言したdeferredのresolveを実行
            defer.resolve();
        }, 1000);

        // 宣言したdeferredからpromiseを取得
        var promise = defer.promise();
        promise.then(function() { 
            // deferredのresolveが実行された後に実行したい処理
            console.log("4番目");
            console.log("5番目");
        });

    });
</script>

こうすることで意図した順番で出力されます。
結果については自身でご確認いただければと思います。
ちなみに下記のように書いた場合はどのように出力されるか想像してみると理解が深まるかもしれません。

<script type="text/javascript">
    $('#startButton').click(function (e) {

        console.log("1番目");

        console.log("2番目");

        // deferredの宣言
        var defer = $.Deferred();

        setTimeout(function () {
            console.log("3番目");
            // 宣言したdeferredのresolveを実行
            defer.resolve();
        }, 1000);

        // 宣言したdeferredからpromiseを取得
        var promise = defer.promise();
        promise.then(function() { 
            // deferredのresolveが実行された後に実行したい処理
            console.log("4番目");
        });

        console.log("5番目");
    });
</script>

終わり

よくネットで説明されているページだと非同期処理を別関数にして説明されているので処理の順番が分かりづらいのかなと思い、この記事を書いてみました。
なのでこちらの内容は説明のために一つの関数の中に非同期処理も含めて書いています。

実際に上記のコードを使用する場合はそもそもdeferredを使用せず、非同期処理の中に続けてしたいことを書くのがベストだと思います。
じゃあdeferredをいつ使うの?となるかもしれませんが、それはよくネットで説明されている「非同期処理を別関数にして記述する」ときになるかと思います。

今回の記事がdeferredを理解し始めるきっかけになれば幸いです。