web-dev-qa-db-ja.com

同期promise解決(bluebirdとjQuery)

Dynamics CRM REST/ODATA Webサービス (CrmRestKit)用の小さなライブラリを開発しました。 libはjQueryに依存し、promise-patternを利用します。これは、jQueryのpromise-like-patternです。

ここで、このlibを bluebird に移植し、jQueryの依存関係を削除するのが好きです。しかし、bluebirdはpromise-objectsの同期解決をサポートしていないため、問題に直面しています。

いくつかのコンテキスト情報:

CrmRestKitのAPIは、Webサービス呼び出しを同期モードと非同期モードのどちらで実行するかを定義するオプションのパラメーターを除きます。

CrmRestKit.Create( 'Account', { Name: "foobar" }, false ).then( function ( data ) {
   ....
} );

「true」を渡すか、最後のパラメーターを省略すると、メソッドは同期してレコードを作成します。モード。

同期モードで操作を実行する必要がある場合があります。たとえば、フォームの保存イベントに含まれるDynamics CRMのJavaScriptコードを記述でき、このイベントハンドラーでは、検証のために同期操作を実行する必要があります(たとえば、特定の数の子レコードが存在することを検証します。適切な数のレコードが存在する場合は、保存操作をキャンセルしてエラーメッセージを表示します)。

私の問題は次のとおりです。bluebirdは同期モードでの解決をサポートしていません。たとえば、次のようにすると、「then」ハンドラーが非同期で呼び出されます。

function print( text ){

    console.log( 'print -> %s', text );

    return text;
}

///
/// 'Promise.cast' cast the given value to a trusted promise. 
///
function getSomeTextSimpleCast( opt_text ){

    var text = opt_text || 'Some fancy text-value';

    return Promise.cast( text );
}

getSomeTextSimpleCast('first').then(print);
print('second');

出力は次のとおりです。

print -> second
print -> first

約束はすでに値で解決されているので、「最初」の後に「2番目」が表示されると思います。したがって、then-event-handlerがすぐに呼び出されると仮定しますすでに解決されたpromise-objectに適用された場合

JQueryで同じことを行うと(解決済みのpromiseで使用)、期待どおりの結果が得られます。

function jQueryResolved( opt_text ){

    var text = opt_text || 'jQuery-Test Value',
    dfd =  new $.Deferred();

    dfd.resolve(text);

        // return an already resolved promise
    return dfd.promise();
}

jQueryResolved('third').then(print);
print('fourth');

これにより、次の出力が生成されます。

print -> third
print -> fourth

ブルーバードを同じように機能させる方法はありますか?

更新:提供されたコードは、問題を説明するためのものです。 libの考え方は次のとおりです。実行モード(sync、async)に関係なく、呼び出し元は常にpromise-objectを処理します。

「...ユーザーに尋ねる...意味がないようです」について:「CreateAsync」と「CreateSync」の2つのメソッドを提供する場合、操作の実行方法を決定するのもユーザーの責任です。

とにかく、現在の実装では、デフォルトの動作(最後のパラメーターはオプション)は非同期実行です。したがって、コードの99%にはpromise-objectが必要であり、オプションのパラメーターは、単に同期の実行が必要な1%の場合にのみ使用されます。さらに、私は自分でlibに開発し、99,9999%のケースで非同期モードを使用していますが、好きなように同期道路に行くオプションがあるのはいいことだと思いました。

しかし、syncメソッドは単に値を返す必要があるという点を理解したと思います。次のリリース(3.0)では、「CreateSync」と「CreateAsync」を実装します。

ご意見ありがとうございます。

pdate-2オプションのパラメーターに対する私の意図は、一貫した動作を保証し、論理エラーを防ぐことでした。 libを使用する私のメソッド「GetCurrentUserRoles」のコンシューマーとしてあなたを想定します。したがって、メソッドは常にpromiseを返します。つまり、結果に応じてコードを実行するには、「then」メソッドを使用する必要があります。したがって、このようなコードを書く人がいるとき、私はそれが完全に間違っていることに同意します。

var currentUserRoels = null;

GetCurrentUserRoles().then(function(roles){

    currentUserRoels = roles;
});

if( currentUserRoels.indexOf('foobar') === -1 ){

    // ...
}

メソッド「GetCurrentUserRoles」がsyncからasyncに変わると、このコードが壊れることに同意します。

しかし、これは良い設計ではないことを理解しています。なぜなら、消費者は非同期メソッドを扱うようになったはずだからです。

12
thuld

短いバージョン:なぜあなたがそれをしたいのか分かりますが、答えはノーです。

質問されている根本的な質問は、約束がすでに完了している場合、完了した約束がすぐにコールバックを実行する必要があるかどうかだと思います。これが発生する可能性のある多くの理由を考えることができます。たとえば、変更が加えられた場合にのみデータを保存する非同期保存手順です。外部リソースを経由せずにクライアント側からの変更を同期的に検出できる場合もありますが、変更が検出された場合にのみ、非同期操作が必要になります。

非同期呼び出しがある他の環境では、開発者が作業がすぐに完了する可能性があることを理解する責任があるというパターンのようです(たとえば、.NET Frameworkの非同期パターンの実装はこれに対応しています)。これはフレームワークの設計上の問題ではなく、実装方法です。

JavaScriptの開発者(および上記のコメント投稿者の多く)は、これについて異なる見解を持っているようで、何かが非同期である可能性がある場合、それは常に非同期でなければならないと主張しています。これが「正しい」かどうかは重要ではありません-私が見つけた仕様によると https://promisesaplus.com/ 、項目2.2.4は、基本的に、外出するまでコールバックを呼び出すことができないと述べています私が「スクリプトコード」または「ユーザーコード」と呼ぶものの;つまり、仕様では、Promiseが完了しても、すぐにコールバックを呼び出すことはできないと明確に規定されています。私は他のいくつかの場所をチェックしました、そして彼らはトピックについて何も言わないか、元の情報源に同意します。 https://promisesaplus.com/ がこの点で決定的な情報源と見なされるかどうかはわかりませんが、私が見た他の情報源はそれに同意せず、それが最も多いようですコンプリート。

この制限はやや恣意的であり、私は率直に言って、これについては.NETの観点を好みます。非同期に見える方法で同期するかもしれないし、しないかもしれない何かをすることが「悪いコード」であると彼らが考えるかどうかを決めるのは他の人に任せます。

実際の質問は、JavaScript以外の動作を実行するようにBluebirdを構成できるかどうかです。パフォーマンスに関しては、そうすることには小さな利点があるかもしれません。JavaScriptでは、十分に努力すれば何でも可能ですが、Promiseオブジェクトがプラットフォーム間でよりユビキタスになるにつれて、カスタム記述ではなくネイティブコンポーネントとして使用するようになります。ポリフィルまたはライブラリ。そのため、今日の答えが何であれ、Bluebirdでpromiseを作り直すと、将来問題が発生する可能性があります。また、promiseに依存したり、すぐに解決したりするようにコードを記述しないでください。

17
Joe Friesenhan

する方法がないので、これは問題だと思うかもしれません

getSomeText('first').then(print);
print('second');

解像度が同期している場合は、"first"の前にgetSomeText"second"を出力します。

しかし、私はあなたが論理的な問題を抱えていると思います。

getSomeText関数が同期である可能性がある場合または非同期である場合、コンテキストによっては、実行の順序に影響を与えることはありません。あなたはそれが常に同じであることを保証するために約束を使用します。実行順序が可変であると、アプリケーションのバグになる可能性があります。

使用する

getSomeText('first') // may be synchronous using cast or asynchronous with ajax
.then(print)
.then(function(){ print('second') });

どちらの場合も(castと同期または非同期の解決)、正しい実行順序になります。

関数が同期している場合とそうでない場合があることに注意してください(キャッシュ処理やプーリングについて考えてみてください)。あなたはそれが非同期であると仮定する必要があります、そしてすべては常にうまくいくでしょう。

しかし、APIのユーザーに、操作を非同期にするかどうかをブール引数で正確に要求することは、JavaScriptの領域を離れない場合(つまり、ネイティブコードを使用しない場合)には意味がないようです。 )。

8
Denys Séguret

約束のポイントは、非同期コードを簡単にすることです。つまり、同期コードを使用するときに感じるものに近づけることです。

同期コードを使用しています。もっと複雑にしないでください。

function print( text ){

    console.log( 'print -> %s', text );

    return text;
}

function getSomeTextSimpleCast( opt_text ){

    var text = opt_text || 'Some fancy text-value';

    return text;
}

print(getSomeTextSimpleCast('first'));
print('second');

そして、それで終わりです。


コードが同期している場合でも同じ非同期インターフェイスを維持したい場合は、ずっとそれを行う必要があります。

getSomeTextSimpleCast('first')
    .then(print)
    .then(function() { print('second'); });

thenは、非同期であると想定されているため、コードを通常の実行フローから外します。 Bluebirdはそこで正しい方法でそれを行います。それが何をするかについての簡単な説明:

function then(fn) {
    setTimeout(fn, 0);
}

bluebirdは実際にはそれを行わないことに注意してください、それはあなたに簡単な例を与えるためだけです。

それを試してみてください!

then(function() {
    console.log('first');
});
console.log('second');

これにより、次のように出力されます。

second
first 
7

ここにはすでにいくつかの良い答えがありますが、問題の核心を非常に簡潔に要約すると:

非同期である場合と同期である場合があるpromise(または他の非同期API)を持つことは悪いことです。

APIの最初の呼び出しでは、同期と非同期を切り替えるためにブール値が必要になるため、問題ないと思われるかもしれません。しかし、それがラッパーコードに埋もれていて、thatコードを使用している人が、これらのシェナニガンについて知らない場合はどうでしょうか。彼らは、彼ら自身の過失によらず、いくつかの実行不可能な振る舞いに終わったばかりです。

結論:これをやろうとしないでください。同期動作が必要な場合は、promiseを返さないでください。

それでは、 You Do n't Know JS からのこの引用を残しておきます:

もう1つの信頼の問題は、「早すぎる」と呼ばれています。アプリケーション固有の用語では、これには、いくつかの重要なタスクが完了する前に呼び出されることが実際に含まれる場合があります。しかし、より一般的には、問題は、現在(同期的に)提供するコールバックまたは後で(非同期的に)提供するコールバックを呼び出すことができるユーティリティで明らかです。

同期または非同期の動作に関するこの非決定性は、ほとんどの場合、バグの追跡が非常に困難になります。一部のサークルでは、Zalgoという名前の架空の狂気を誘発するモンスターが同期/非同期の悪夢を説明するために使用されます。 「Zalgoをリリースしないでください!」これはよくある叫びであり、非常に適切なアドバイスにつながります。イベントループの次のターンで「すぐに」コールバックを呼び出す場合でも、すべてのコールバックが予想どおりに非同期になるように、常に非同期でコールバックを呼び出します。

注:Zalgoの詳細については、Oren Golanの「Don'tReleaseZalgo!」を参照してください。 ( https://github.com/oren/oren.github.io/blob/master/posts/zalgo.md )およびIsaac Z. Schlueterの「DesigningAPIs for Asynchrony」( http://blog.izs.me/post/59142742143/designing-apis-for-asynchrony )。

考えてみましょう:

function result(data) {
    console.log( a );
}

var a = 0;

ajax( "..pre-cached-url..", result );
a++;`

このコードは0(同期コールバック呼び出し)または1(非同期コールバック呼び出し)を出力しますか?条件によって異なります。

Zalgoの予測不可能性がJSプログラムをどれほど迅速に脅かすことができるかがわかります。したがって、愚かな響きの「Zalgoをリリースしない」というのは、実際には信じられないほど一般的で確かなアドバイスです。常に非同期である。

2
JLRishe

この場合はどうでしょうか。また、最新バージョンではBluebirdを使用するCrmFetchKit関連もあります。 jQueryに基づいたバージョン1.9からアップグレードしました。それでも、CrmFetchKitを使用する古いアプリコードには、プロトタイプを変更できない、または変更しないメソッドがあります。

既存のアプリコード

CrmFetchKit.FetchWithPaginationSortingFiltering(query.join('')).then(
    function (results, totalRecordCount) {
        queryResult = results;

        opportunities.TotalRecords = totalRecordCount;

        done();
    },
    function err(e) {
        done.fail(e);
    }
);

古いCrmFetchKitの実装(fetch()のカスタムバージョン)

function fetchWithPaginationSortingFiltering(fetchxml) {

    var performanceIndicator_StartTime = new Date();

    var dfd = $.Deferred();

    fetchMore(fetchxml, true)
        .then(function (result) {
            LogTimeIfNeeded(performanceIndicator_StartTime, fetchxml);
            dfd.resolve(result.entities, result.totalRecordCount);
        })
        .fail(dfd.reject);

    return dfd.promise();
}

新しいCrmFetchKitの実装

function fetch(fetchxml) {
    return fetchMore(fetchxml).then(function (result) {
        return result.entities;
    });
}

私の問題は、古いバージョンにはdfd.resolve(...)があり、必要な数のパラメーターを渡すことができたということです。

新しい実装が返されるだけで、親がコールバックを呼び出しているようです。直接呼び出すことはできません。

私は行って、新しい実装でfetch()のカスタムバージョンを作成しました

function fetchWithPaginationSortingFiltering(fetchxml) {
    var thePromise = fetchMore(fetchxml).then(function (result) {
        thePromise._fulfillmentHandler0(result.entities, result.totalRecordCount);
        return thePromise.cancel();
        //thePromise.throw();
    });

    return thePromise;
}

しかし、問題は、コールバックが2回呼び出されることです。1回は明示的に呼び出し、2回目はフレームワークによって呼び出されますが、1つのパラメーターのみが渡されます。それをだまして、明示的に行うので何も呼び出さないように「伝える」ために、.cancel()を呼び出そうとしますが、無視されます。理由は理解しましたが、それでも「dfd.resolve(result.entities、result.totalRecordCount);」をどのように実行しますか。このライブラリを使用するアプリのプロトタイプを変更する必要のない新しいバージョンでは?

0
Nicolas