JavaScriptの中の非同期の整理(2)——Promises/Aを使う

97371 ワード

Promisesは、非同期プログラミングモデルであり、非同期動作を規範化するAPIのセットによって、非同期動作の流れ制御をより容易にすることができる.ここで話しているのはPromises/Aです.Promisesの一つの分岐です.PromisesモデルによってAPIのセットが定義されています.Promisesは初心者にとって曲線を理解するのはまだ急なので、ここでは順を追って紹介します.同時に簡単なPromises/Aコードを実現します.Promises/Aに別名「thenable」があります.これは「then」です.このプロミセには「デフォルト、完了、失敗」という3つの状態があります.最初に作成したときはデフォルトの状態です.状態はデフォルトから完成になります.またはデフォルトは失敗になります.いったん完成したり失敗したりしたら、状態はもう変えられません.文章を簡単にするために、ここではまず完成だけを考えて、失敗を考えません.
1
2
3
4
5
6
7
8
9
10
11
var Promise = function(ok){
    this.state = 'unfulfilled';
    this.ok =  || function(obj) { return obj; };
};
Promise.prototype = {
    resolve: function(obj){
        if (this.state !== 'unfulfilled') throw '   ,    resolve';
        this.state = 'fulfilled';
    }
};
var promise = new Promise(function(obj){ return obj; });
コンストラクタのokはタスクであり、promise.resove(obj)はこのプロミセの状態を完了に変更すると、そこでokが実行され、その戻り値は後の動作のパラメータおよびresoveの戻り値として返されます.非同期操作との関連がないため、ここのPromiseはまだ何の役割も果たしていません.Promises/Aが「thenable」と呼ばれるのは、コアAPIをthenといい、望文生義という方法の役割は、プロミセが完成した時や失敗した時に他のことを続けるからです.
thenはパラメータnextOK①として関数を導入し、このpromiseがresoveされると、resoveの戻り値がnextOKに伝達されます.thenはpromiseを返します.上記の後の操作が完了すると、戻ってきたpromiseもresoliveされます.プロミスの状態が完了したら、nextOKはすぐに呼び出されます.しかし、このように非同期ではないので、nextOKの戻り値がPromiseであれば、thenが戻ってくるプロミスはこのプロミセがresoliveされた時にresoveされるという特殊な状況があります.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var Promise = function(ok){
    this.state = 'unfulfilled';
    this.ok = ok || function(obj) { return obj; };
    this.thens = [];
};
Promise.prototype = {
    resolve: function(obj){
        if (this.state != 'unfulfilled') throw '   ,    resolve';
        this.state = 'fulfilled';
        this.result = this.ok(obj); //   ok
 
        for (var i=0, len=this.thens.length; i<len; ++i){
            //             
            var then = this.thens[i];
            this._fire(then.promise, then.ok);
        }
        return this;
    },
    _fire: function(nextPromise, nextOK){
        var nextResult = nextOK(this.result); //   nextOK
        if (nextResult instanceof Promise){
            //      ,      Promise,   resolve   ,nextPromise   resolve
            nextResult.then(function(obj){
                nextPromise.resolve(obj);
            });
        }else{
            //      ,        ,   nextPromise resolve 
            nextPromise.resolve(nextResult);
        }
        return nextPromise;
    },
    _push: function(nextPromise, nextOK){
        this.thens.push({
            promise: nextPromise,
            ok: nextOK
        });
        return nextPromise;
    },
    then: function(nextOK){
        var promise = new Promise();
        if (this.state == 'fulfilled'){
            //           , nextOK      
            return this._fire(promise, nextOK);
        }else{
            //           
            return this._push(promise, nextOK);
        }
    }
};
ここに来て、私達の極簡版Promiseが完成しました.どうやって使いますか?ここで例を挙げると、まず「タスク」を定義します.
1
2
3
4
5
6
7
function print(num){
    console.log(num);
    return num;
}
function addTwo(num){
    return num + 2;
}
必要に応じてこれらの任務を組織する.
1
2
3
4
5
6
var promise = new Promise(print);
promise.then(addTwo)
       .then(print)
       .then(addTwo)
       .then(print); //              
promise.resolve(3); //       
コンソールから順に3、5、7がプリントされているのが見えます.しかし、これらのタスクは同期しています.Promiseの強さを表現できません.ここではnextOKを通じてpromiseに戻る方法でdelayを実現します.
1
2
3
4
5
6
7
8
9
function delay(ms){
    return function(obj){
        var promise = new Promise();
        setTimeout(function(){
            promise.resolve(obj);
        }, ms);
        return promise;
    };
}
これを利用して上のジョブキューを改造し、後の2回の印刷間を2秒遅らせます.
1
2
3
4
5
6
7
var promise = new Promise(print);
promise.then(addTwo)
       .then(print)
       .then(delay(2000)) //   2 
       .then(addTwo)
       .then(print);
promise.resolve(3);
この原理を利用して、いくつかの巧妙なコードを作ることができます.
1
2
3
4
5
6
7
8
9
10
11
12
13
function fibNext(pair){
    print(pair[0]);
    return [pair[1], pair[0]+pair[1]];
}
 
var promise = new Promise(fibNext);
promise.then(function(pair){
    promise = promise.then(delay(1000))
                     .then(fibNext)
                     .then(arguments.callee);
    return pair;
});
promise.resolve([1,1]);
上にはループは使用されていませんが、1秒ごとに無限に自動印刷されたフィボナッチの数列が実現されました.
Promisesモデルはかなり優雅であり、幾つかの拡張によりwhen,whenAllなどのAPIが実現され、パッケージ非同期動作に非常に役立つ.実際のライブラリではPromiseという名前はあまり使われていませんが、Deferredは「遅延」という意味で使われていますので、Deferredは「遅延列」または「非同期列」と呼ばれています.jQuery 1.5にjQuery.Deferredを導入しました.Dojoはこの点で先行者です.dojo 0.3はDeferredを実現しました.実際にDeferredを使用した後、jQuery.ajaxとdojo.ajaxが戻ってきた結果はDeferredです.したがって、伝統的な着信コールバック関数の代わりにthenを使うことができます.
1
2
3
4
5
6
7
8
dojo.xhrGet({ 
    url: "users.json", 
    handleAs: "json" 
}).then(function(userList){ 
    dojo.forEach(userList, function(user){
        appendUser(user);
    }); 
});
このようなコードを使用すると、いつでもajaxに対してフィードバックの追加を要求できます.必ずしも定義の最初にリピートを設定するのではなく、柔軟性が強いです.「一連の関数を設定して、適切な時にそれらを呼び出します.その後に加入する関数はすぐに呼び出されます.」という特性は、まるで生まれつきのdomReadyとペアです.実はjQueryもDeferredを使って再構成されました.同時に、Deferredによってアニメーションを実現するという連続的かつ並行的な非同期的なミッションも非常に優雅です.
Promisesモデルを通して、非同期の操作を非同期の「タスク」として認識し、タスク単位で非同期の操作をアレンジしていますが、実際にはもうちょっと関数的な味がします.次の文章は、このシリーズの最後の編でもあります.もう一つのもっと関数的なJavaScript非同期操作組織方法を紹介します.
①事実上Promises/Aの定義は複雑で、失敗rejectなどを含めて詳細には説明しない.