web-dev-qa-db-ja.com

以前の約束の結果にどのようにして.then()チェーンにアクセスするのですか?

私は自分のコードを promise に再構成し、複数のものからなる、すばらしいlongフラットなpromiseチェーンを構築しました。 .then()コールバック最後に、私はいくつかの複合値を返したい、そして複数の中間約束結果にアクセスする必要がある。しかし、シーケンスの途中からの解像度の値は、最後のコールバックでは有効範囲にありません。どうやってそれらにアクセスするのですか?

function getExample() {
    return promiseA(…).then(function(resultA) {
        // Some processing
        return promiseB(…);
    }).then(function(resultB) {
        // More processing
        return // How do I gain access to resultA here?
    });
}
582
Bergi

ECMAScriptハーモニー

もちろん、この問題は言語デザイナーによっても認識されていました。彼らは多くの仕事をしました、そして 非同期関数の提案 はついにそれを次のようにしました。

ECMAScript 8

非同期関数(呼び出されたときにpromiseを返す)のように、promiseが直接解決されるのを待つだけでよいので、thenの呼び出しやコールバック関数はもう必要ありません。また、条件、ループ、try-catch-句などの任意の制御構造も備えていますが、便宜上、ここでは必要ありません。

async function getExample() {
    var resultA = await promiseA(…);
    // some processing
    var resultB = await promiseB(…);
    // more processing
    return // something using both resultA and resultB
}

ECMAScript 6

私たちがES8を待っている間、私たちはすでに非常によく似た種類の構文を使いました。 ES6には ジェネレータ関数 が付属しています。これにより、任意の場所に配置されたyieldキーワードで実行を分割することができます。これらのスライスは、独立して、非同期でさえも、順番に実行することができます - そしてそれは、次のステップを実行する前に、約束の解決を待つときに行うことです。

専用のライブラリ( cotask.js など)がありますが、多くのプロミスライブラリにはヘルパー関数があります( QBluebirdの場合 、…) あなたが彼らに約束を生み出すジェネレータ関数を与えるとき)あなたのために)この非同期ステップバイステップの実行

var getExample = Promise.coroutine(function* () {
//               ^^^^^^^^^^^^^^^^^ Bluebird syntax
    var resultA = yield promiseA(…);
    // some processing
    var resultB = yield promiseB(…);
    // more processing
    return // something using both resultA and resultB
});

これは、バージョン4.0以降のNode.jsで機能しました。また、いくつかのブラウザ(またはそれらの開発版)は、比較的早期にジェネレータ構文をサポートしていました。

ECMAScript 5

しかし、後方互換性が必要な場合は、transpilerなしでは使用できません。ジェネレータ関数と非同期関数は現在のツールでサポートされています。例えば、 ジェネレータ非同期関数 に関するBabelのドキュメントを参照してください。

それから、非同期プログラミングを容易にすることを目的とした、他の JSコンパイル言語 もあります。それらは通常await、(例えば Iced CoffeeScript )に似た構文を使いますが、Haskellのようなdo-表記を特徴とするものもあります(例えば LatteJsモナディックPureScript または LispyScript )。

211
Bergi

鎖を壊す

チェーンの中間値にアクセスする必要がある場合は、チェーンを必要な部分に分割してください。 1つのコールバックをアタッチして、どういうわけかそのパラメータを複数回使用しようとする代わりに、同じ約束に複数のコールバックをアタッチします - あなたが結果の値を必要とするときはいつでも。忘れないでください、 の約束は未来の価値 を表しているだけです。線形チェーンで一方の約束を他方の約束から派生させることの次に、結果の値を構築するためにあなたのライブラリからあなたに与えられた約束コンビネータを使用してください。

これにより、非常に簡単な制御フロー、明確な機能の構成、そしてそれによる容易なモジュール化がもたらされる。

function getExample() {
    var a = promiseA(…);
    var b = a.then(function(resultA) {
        // some processing
        return promiseB(…);
    });
    return Promise.all([a, b]).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

ES6でのみ使用可能になったPromise.allの後のコールバックでのパラメータのデストラクタリングの代わりに、ES5ではthen呼び出しは多くのプロミスライブラリによって提供されていた気の利いたヘルパーメソッドに置き換えられます( Q )、 Bluebirdの場合 、…):.spread(function(resultA, resultB) { …

Bluebirdには、そのPromise.all + joinの組み合わせをより単純な(そしてより効率的な)構文に置き換えるための専用 spread関数 もあります。

…
return Promise.join(a, b, function(resultA, resultB) { … });
341
Bergi

同期検査

後で必要な値の約束を変数に割り当ててから、同期検査を介してその値を取得します。この例ではbluebirdの.value()メソッドを使用していますが、多くのライブラリが同様のメソッドを提供しています。

function getExample() {
    var a = promiseA(…);

    return a.then(function() {
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // a is guaranteed to be fulfilled here so we can just retrieve its
        // value synchronously
        var aValue = a.value();
    });
}

これは好きなだけ多くの値に使用することができます。

function getExample() {
    var a = promiseA(…);

    var b = a.then(function() {
        return promiseB(…)
    });

    var c = b.then(function() {
        return promiseC(…);
    });

    var d = c.then(function() {
        return promiseD(…);
    });

    return d.then(function() {
        return a.value() + b.value() + c.value() + d.value();
    });
}
97
Esailija

ネスト(および)クロージャー

変数のスコープ(この場合、成功コールバック関数パラメーター)を維持するためにクロージャーを使用するのは、自然なJavaScriptソリューションです。 promiseを使用すると、任意に nest and flatten.then()コールバックを実行できます-内側のスコープを除き、それらは意味的に同等です。

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(function(resultB) {
            // more processing
            return // something using both resultA and resultB;
        });
    });
}

もちろん、これはインデントピラミッドを構築しています。インデントが大きくなりすぎている場合でも、古いツールを適用して Doom of Doom に対抗できます。モジュール化、追加の名前付き関数の使用、変数が不要になり次第プロミスチェーンのフラット化もう。
理論的には、(すべてのクロージャーを明示的にすることで)常に2レベル以上のネストを回避でき、実際には合理的な範囲で使用します。

function getExample() {
    // preprocessing
    return promiseA(…).then(makeAhandler(…));
}
function makeAhandler(…)
    return function(resultA) {
        // some processing
        return promiseB(…).then(makeBhandler(resultA, …));
    };
}
function makeBhandler(resultA, …) {
    return function(resultB) {
        // more processing
        return // anything that uses the variables in scope
    };
}

この種類のヘルパー関数を使用することもできます 部分的なアプリケーション 、たとえば_.partial from nderscore / lodash または native .bind() method 、インデントをさらに減らすため:

function getExample() {
    // preprocessing
    return promiseA(…).then(handlerA);
}
function handlerA(resultA) {
    // some processing
    return promiseB(…).then(handlerB.bind(null, resultA));
}
function handlerB(resultA, resultB) {
    // more processing
    return // anything that uses resultA and resultB
}
51
Bergi

明示的なパススルー

コールバックをネストするのと同様に、この手法はクロージャに依存しています。それでも、チェーンはフラットのままです。最新の結果だけを渡すのではなく、ステートオブジェクトがすべてのステップで渡されます。これらの状態オブジェクトは、前のアクションの結果を蓄積し、後で必要になるすべての値と現在のタスクの結果を渡します。

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(b => [resultA, b]); // function(b) { return [resultA, b] }
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

ここでは、その小さな矢印b => [resultA, b]resultAを閉じて両方の結果の配列を次のステップに渡す関数です。これはパラメータ分解構文を使用して、単一の変数に分割します。

破壊がES6で利用可能になる前に、.spread()と呼ばれる気の利いたヘルパーメソッドが多くの約束のライブラリによって提供されました( QBluebirdとき 、…)。これは、.spread(function(resultA, resultB) { …として使用される複数のパラメータ(各配列要素に対して1つ)を持つ関数を取ります。

もちろん、ここで必要とされるクロージャーはいくつかのヘルパー関数によってさらに単純化することができます。

function addTo(x) {
    // imagine complex `arguments` fiddling or anything that helps usability
    // but you get the idea with this simple one:
    return res => [x, res];
}

…
return promiseB(…).then(addTo(resultA));

あるいは、Promise.allを使って配列の約束を生成することもできます。

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return Promise.all([resultA, promiseB(…)]); // resultA will implicitly be wrapped
                                                    // as if passed to Promise.resolve()
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

また、配列だけでなく、任意に複雑なオブジェクトを使用することもできます。たとえば、 _.extend または Object.assign を別のヘルパー関数で使用すると、次のようになります。

function augment(obj, name) {
    return function (res) { var r = Object.assign({}, obj); r[name] = res; return r; };
}

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(augment({resultA}, "resultB"));
    }).then(function(obj) {
        // more processing
        return // something using both obj.resultA and obj.resultB
    });
}

このパターンはフラットチェーンを保証し、明示的な状態オブジェクトは明確さを向上させることができますが、長いチェーンにとっては面倒になります。特にあなたが状態を散発的にしか必要としないとき、あなたはまだすべてのステップを通してそれを通過しなければなりません。この固定されたインタフェースでは、チェーン内の単一のコールバックはかなり密接に結合されており、変更に柔軟性がありません。それは単一のステップを取り除くことをより困難にします、そしてコールバックは他のモジュールから直接供給されることができません - それらは常に状態を気にする定型コードで包まれる必要があります。上記のような抽象ヘルパー関数は痛みを少し軽減することができますが、常に存在します。

48
Bergi

変更可能なコンテキスト状態

些細な(しかし巧妙ではなくエラーが発生しやすい)解決策は、(チェーン内のすべてのコールバックがアクセス可能な)よりスコープの広い変数を使用し、取得時に結果値を書き込むことです。

function getExample() {
    var resultA;
    return promiseA(…).then(function(_resultA) {
        resultA = _resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both resultA and resultB
    });
}

多くの変数の代わりに、(最初は空の)オブジェクトを使用することもできます。このオブジェクトには、結果が動的に作成されたプロパティとして格納されます。

この解決策にはいくつかの欠点があります。

  • 可変状態は醜いグローバル変数は悪 です。
  • このパターンは関数の境界を越えては機能しません。関数をモジュール化することは宣言が共有されたスコープを離れてはいけないので難しいです。
  • 変数の範囲は、それらが初期化される前にそれらにアクセスすることを妨げません。これは、競合状態が発生する可能性がある複雑なプロミス構成(ループ、分岐、除外)に特に適しています。明示的にstateを渡すと、 declarative design が推奨され、これを防ぐことができるよりクリーンなコーディングスタイルが強制されます。
  • それらのシェア変数のスコープを正しく選択する必要があります。たとえば、状態がインスタンスに格納されている場合のように、複数の並列呼び出しの間の競合状態を防ぐためには、実行された関数に対してローカルである必要があります。

Bluebirdライブラリは、 それらのbind()メソッド を使用して、約束されたチェーンにコンテキストオブジェクトを割り当てることで、一緒に渡されるオブジェクトの使用を推奨します。それはそうでなければ使用できない thisキーワード を介して各コールバック関数からアクセス可能になります。オブジェクトのプロパティは変数よりも検出されないタイプミスが発生しやすいのですが、パターンはかなり巧妙です。

function getExample() {
    return promiseA(…)
    .bind({}) // Bluebird only!
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }).bind(); // don't forget to unbind the object if you don't want the
               // caller to access it
}

このアプローチは、.bindをサポートしないpromiseライブラリで簡単にシミュレートできます(ただし多少冗長な方法であり、式では使用できません)。

function getExample() {
    var ctx = {};
    return promiseA(…)
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }.bind(ctx)).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }.bind(ctx));
}
32
Bergi

「変更可能なコンテキスト状態」に関するそれほど厳しくないスピン

ローカルスコープのオブジェクトを使用して中間結果をプロミスチェーンに集めることは、あなたが提起した質問に対する合理的なアプローチです。次のコードを見てください。

function getExample(){
    //locally scoped
    const results = {};
    return promiseA(...).then(function(resultA){
        results.a = resultA;
        return promiseB(...);
    }).then(function(resultB){
        results.b = resultB;
        return promiseC(...);
    }).then(function(resultC){
        //Resolve with composite of all promises
        return Promise.resolve(results.a + results.b + resultC);
    }).catch(function(error){
        return Promise.reject(error);
    });
}
  • グローバル変数は良くないので、この解決法はローカルスコープの変数を使用しますが、これは害を及ぼしません。関数内でのみアクセス可能です。
  • 可変状態は醜いですが、これは醜い方法で状態を変更しません。醜い可変状態は伝統的に関数の引数やグローバル変数の状態を変更することを意味しますが、このアプローチは単に約束の結果を集約することのみを目的としたローカルスコープ変数の状態を変更するだけです。約束が解決したら。
  • 中間の約束が結果オブジェクトの状態にアクセスするのを妨げることはできませんが、チェーン内の約束の1つが不正になり、結果が破壊されるような恐ろしいシナリオは発生しません。約束の各ステップで値を設定する責任はこの関数に限られていて、全体的な結果は正しいか間違っているかのどちらかです。 !)
  • GetExample関数を呼び出すたびにresults変数の新しいインスタンスが作成されるため、これは並列呼び出しから生じる競合状態のシナリオを導入するものではありません。
9
Jay

ノード7.4は、ハーモニーフラグ付きの非同期/待機呼び出しをサポートします。

これを試して:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

ファイルを次のように実行します。

node --harmony-async-await getExample.js

することができますようにシンプル!

7
Antoine

このごろ、私はあなたのようないくつかの質問にも出会っています。ついに、私は質問に良い解決策を見つける、それは読むのが簡単で良いです。これがお役に立てば幸いです。

how-to-chain-javascript-promisesに従って

それでは、コードを見てみましょう。

const firstPromise = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('first promise is completed');
            resolve({data: '123'});
        }, 2000);
    });
};

const secondPromise = (someStuff) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('second promise is completed');
            resolve({newData: `${someStuff.data} some more data`});
        }, 2000);
    });
};

const thirdPromise = (someStuff) => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('third promise is completed');
            resolve({result: someStuff});
        }, 2000);
    });
};

firstPromise()
    .then(secondPromise)
    .then(thirdPromise)
    .then(data => {
        console.log(data);
    });
7
yzfdjzwl

babel-node version <6を使ったもう一つの答え

async - awaitを使う

npm install -g [email protected]

example.js:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

それから、babel-node example.jsを実行してください。

5
Antoine

別の答えは、シーケンシャルエグゼキュータ nsynjs を使用することです。

function getExample(){

  var response1 = returnPromise1().data;

  // promise1 is resolved at this point, '.data' has the result from resolve(result)

  var response2 = returnPromise2().data;

  // promise2 is resolved at this point, '.data' has the result from resolve(result)

  console.log(response, response2);

}

nynjs.run(getExample,{},function(){
    console.log('all done');
})

更新:実施例を追加

function synchronousCode() {
     var urls=[
         "https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js",
         "https://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js",
         "https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"
     ];
     for(var i=0; i<urls.length; i++) {
         var len=window.fetch(urls[i]).data.text().data.length;
         //             ^                   ^
         //             |                   +- 2-nd promise result
         //             |                      assigned to 'data'
         //             |
         //             +-- 1-st promise result assigned to 'data'
         //
         console.log('URL #'+i+' : '+urls[i]+", length: "+len);
     }
}

nsynjs.run(synchronousCode,{},function(){
    console.log('all done');
})
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>
2
amaksr

Bluebirdを使うときは、.bindメソッドを使ってプロミスチェーンで変数を共有することができます。

somethingAsync().bind({})
.spread(function (aValue, bValue) {
    this.aValue = aValue;
    this.bValue = bValue;
    return somethingElseAsync(aValue, bValue);
})
.then(function (cValue) {
    return this.aValue + this.bValue + cValue;
});

詳細については、このリンクを確認してください。

http://bluebirdjs.com/docs/api/promise.bind.html

1
alphakevin
function getExample() {
    var retA, retB;
    return promiseA(…).then(function(resultA) {
        retA = resultA;
        // Some processing
        return promiseB(…);
    }).then(function(resultB) {
        // More processing
        //retA is value of promiseA
        return // How do I gain access to resultA here?
    });
}

簡単な方法:D

1
Minh Giang

グローバル変数を使用することは大好きではないので、このパターンを自分のコードで使用するつもりはありません。しかし、ピンチでそれは動作します。

ユーザーは、公認のマングースモデルです。

var globalVar = '';

User.findAsync({}).then(function(users){
  globalVar = users;
}).then(function(){
  console.log(globalVar);
});
1
Antoine

RSVPのハッシュが使えると思います。

以下のようなもの:

    const mainPromise = () => {
        const promise1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('first promise is completed');
                resolve({data: '123'});
            }, 2000);
        });

        const promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('second promise is completed');
                resolve({data: '456'});
            }, 2000);
        });

        return new RSVP.hash({
              prom1: promise1,
              prom2: promise2
          });

    };


   mainPromise()
    .then(data => {
        console.log(data.prom1);
        console.log(data.prom2);
    });
1
Vishu

溶液:

「bind」を使用して、後の「then」関数のスコープに中間値を明示的に配置できます。これは、Promiseの動作を変更する必要がなく、エラーが既に伝播されているように値を伝播するのに1行または2行のコードのみを必要とするニースのソリューションです。

完全な例を次に示します。

// Get info asynchronously from a server
function pGetServerInfo()
    {
    // then value: "server info"
    } // pGetServerInfo

// Write into a file asynchronously
function pWriteFile(path,string)
    {
    // no then value
    } // pWriteFile

// The heart of the solution: Write formatted info into a log file asynchronously,
// using the pGetServerInfo and pWriteFile operations
function pLogInfo(localInfo)
    {
    var scope={localInfo:localInfo}; // Create an explicit scope object
    var thenFunc=p2.bind(scope); // Create a temporary function with this scope
    return (pGetServerInfo().then(thenFunc)); // Do the next 'then' in the chain
    } // pLogInfo

// Scope of this 'then' function is {localInfo:localInfo}
function p2(serverInfo)
    {
    // Do the final 'then' in the chain: Writes "local info, server info"
    return pWriteFile('log',this.localInfo+','+serverInfo);
    } // p2

このソリューションは、次のように呼び出すことができます。

pLogInfo("local info").then().catch(err);

(注:このソリューションのより複雑で完全なバージョンがテストされていますが、このサンプルバージョンではないため、バグがある可能性があります。)

0
David Spector