web-dev-qa-db-ja.com

約束-約束を強制的にキャンセルすることは可能ですか

ES6 Promisesを使用してすべてのネットワークデータ取得を管理しますが、それらを強制的にキャンセルする必要がある状況がいくつかあります。

基本的にシナリオは、リクエストがバックエンドに委任されるUIで先行入力検索を行い、部分入力に基づいて検索を実行する必要があるようなものです。このネットワークリクエスト(#1)には少し時間がかかる場合がありますが、ユーザーは入力を続け、最終的に別のバックエンドコールをトリガーします(#2)

ここでは、#2が自然に#1より優先されるため、Promiseラッピングリクエスト#1をキャンセルしたいと思います。私はすでにデータ層にすべてのPromiseのキャッシュを持っているので、#2のPromiseを送信しようとしているときに理論的にそれを取得できます。

しかし、キャッシュから取得したPromise#1をキャンセルするにはどうすればよいですか?

誰かがアプローチを提案できますか?

66
Moonwalker

いいえ、まだできません。

ES6 promiseはキャンセルをサポートしていませんyet。それは進行中であり、そのデザインは多くの人々が本当に一生懸命取り組んだものです。 Soundキャンセルのセマンティクスを正しく理解するのは難しく、これは進行中の作業です。 「fetch」レポ、esdiscuss、およびGHの他のいくつかのレポジトリについては興味深い議論がありますが、私があなただったら辛抱強く待っています。

しかし、しかし、しかし..キャンセルは本当に重要です!

問題は、キャンセルが本当にクライアント側プログラミングの重要なシナリオであるという現実です。 Webリクエストの中止など、あなたが説明するケースは重要であり、どこにでもあります。

だから...言語は私を台無しにしました!

ええ、ごめんなさい。約束は、さらに物事が指定される前に最初に入らなければなりませんでした-したがって、.finallyおよび.cancel-しかし、DOMを介して仕様に向かっています。キャンセルはnotであり、単なる時間の制約であり、API設計に対するより反復的なアプローチです。

じゃあどうすればいい?

いくつかの選択肢があります:

  • bluebird のようなサードパーティライブラリを使用します。このライブラリは仕様よりもはるかに高速に移動できるため、キャンセルやその他の特典があります。これがWhatsAppのような大企業の仕事です。
  • キャンセルtokenを渡します。

サードパーティのライブラリを使用することは非常に明白です。トークンに関しては、次のようにメソッドに関数を取り込んでから呼び出すことができます。

function getWithCancel(url, token) { // the token is for cancellation
   var xhr = new XMLHttpRequest;
   xhr.open("GET", url);
   return new Promise(function(resolve, reject) {
      xhr.onload = function() { resolve(xhr.responseText); });
      token.cancel = function() {  // SPECIFY CANCELLATION
          xhr.abort(); // abort request
          reject(new Error("Cancelled")); // reject the promise
      };
      xhr.onerror = reject;
   });
};

あなたができるようになります:

var token = {};
var promise = getWithCancel("/someUrl", token);

// later we want to abort the promise:
token.cancel();

実際の使用例-last

これは、トークンアプローチではそれほど難しくありません。

function last(fn) {
    var lastToken = { cancel: function(){} }; // start with no op
    return function() {
        lastToken.cancel();
        var args = Array.prototype.slice.call(arguments);
        args.Push(lastToken);
        return fn.apply(this, args);
    };
}

あなたができるようになります:

var synced = last(getWithCancel);
synced("/url1?q=a"); // this will get canceled 
synced("/url1?q=ab"); // this will get canceled too
synced("/url1?q=abc");  // this will get canceled too
synced("/url1?q=abcd").then(function() {
    // only this will run
});

いいえ、BaconやRxのようなライブラリは観察可能なライブラリであるため、ここで「輝いている」ことはありません。仕様に拘束されないことでユーザーレベルの約束ライブラリと同じ利点を持っています。 ES2016でオブザーバブルがネイティブになるのを待ちます。それらはaretypeaheadにとっては気の利いたものです。

112

キャンセル可能な約束の標準的な提案は失敗しました。

約束は、それを実現する非同期アクションのコントロールサーフェスではありません。所有者と消費者を混同します。代わりに、非同期functionsを作成します。これは、渡されたトークンを介してキャンセルできます。

別の約束は素晴らしいトークンを作り、キャンセルをPromise.raceで簡単に実装できるようにします:

例:Promise.raceを使用して、前のチェーンの効果をキャンセルします。

let cancel = () => {};

input.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancel();
  let p = new Promise(resolve => cancel = resolve);
  Promise.race([p, getSearchResults(term)]).then(results => {
    if (results) {
      console.log(`results for "${term}"`,results);
    }
  });
}

function getSearchResults(term) {
  return new Promise(resolve => {
    let timeout = 100 + Math.floor(Math.random() * 1900);
    setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
  });
}
Search: <input id="input">

ここでは、undefinedの結果を挿入してテストすることにより、以前の検索を「キャンセル」していますが、代わりに"CancelledError"で拒否することを簡単に想像できます。

もちろん、これは実際にはネットワーク検索をキャンセルしませんが、それはfetchの制限です。 fetchが引数としてキャンセルプロミスを受け取る場合、ネットワークアクティビティをキャンセルできます。

proposed このes-discussの「約束のパターンをキャンセルする」、まさにfetchがこれを行うことを示唆しています。

15
jib

私はMozilla JSリファレンスをチェックアウトし、これを見つけました:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race

それをチェックしよう:

_var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, "one"); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, "two"); 
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
  // Both resolve, but p2 is faster
});
_

ここにp1とp2がPromise.race(...)を引数として入れていますが、これは実際に新しい解決プロミスを作成しているので、これが必要です。

6

Promise Extensions for JavaScript(Prex) を使用することをお勧めします。その著者 Ron Buckton はTypeScriptの重要なエンジニアの1人であり、現在のTC39の ECMAScript Cancellation 提案の背後にいる人物でもあります。ライブラリは十分に文書化されており、Prexの一部が標準に準拠する可能性があります。

個人的なメモで、C#の重いバックグラウンドから来た場合、Prexは既存の マネージスレッドでのキャンセル フレームワーク、つまりCancellationTokenSourceで取られたアプローチに基づいてモデル化されているという事実がとても気に入っています/ CancellationToken .NET API。私の経験では、これらはマネージアプリに堅牢なキャンセルロジックを実装するのに非常に便利でした。

Nodeで _prex.CancellationTokenSource_ を使用したキャンセルの遅延の例を次に示します。

_const prex = require('prex');

async function delayWithCancellation(timeoutMs, token) {
  // this can easily be done without async/await,
  // but I believe this linear structure is more readable
  let reg = null;
  try {
    await new Promise((resolve, reject) => {
      const id = setTimeout(resolve, timeoutMs);
      reg = token.register(() => {
        clearTimeout(id);
        reject(new prex.CancelError("delay cancelled."));
      });
    });  
  }
  finally {
    reg && reg.unregister();
  }
}

async function main() {
  const tokenSource = new prex.CancellationTokenSource();
  setTimeout(() => tokenSource.cancel(), 1500); // cancel after 1500ms

  // without cancellation
  await delayWithCancellation(1000, prex.CancellationToken.none);
  console.log("successfully delayed once.");

  // with cancellation
  const token = tokenSource.token;
  await delayWithCancellation(1500, token);
  token.throwIfCancellationRequested();
  console.log("successfully delayed twice."); // we should not be here
}

main().catch(e => console.log(e));
_

キャンセルはレースであることに注意してください。つまり、約束は正常に解決された可能性がありますが、(awaitまたはthenを使用して)確認するまでに、キャンセルもトリガーされた可能性があります。このレースをどのように処理するかはあなた次第ですが、上記のようにtoken.throwIfCancellationRequested()を余分に呼び出すことは決して痛いことはありません。

Updated、標準ネイティブ Promiseクラス をキャンセルサポート付きで拡張しました。これは Bluebirdで実装 (つまり、オプションのoncancelコールバックを使用)、ただし_prex.CancellationTokenSource_を使用します。 CancellablePromiseのコードは利用可能です ここ

3
noseratio

最近、同様の問題に直面しました。

約束ベースのクライアント(ネットワークのクライアントではない)があり、UIをスムーズに保つために、常に最新の要求データをユーザーに提供したかったのです。

キャンセルのアイデアに苦労した後、Promise.race(...)Promise.all(..)私はちょうど最後のリクエストIDを思い出し始め、promiseが満たされたとき、最後のリクエストのIDと一致したときにのみデータをレンダリングしていました。

それが誰かを助けることを願っています。

1
Igor Słomski

https://www.npmjs.com/package/promise-abortable を参照してください

$ npm install promise-abortable
0
Devi