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


95

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

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

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

しかし、キャッシュからPromise#1を取得したら、どうすればキャンセルできますか?

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


2
それは、デバウンス関数に相当するものを使用して、頻繁にトリガーされず、廃止されたリクエストになるオプションですか?300ミリ秒の遅延でうまくいくとしましょう。たとえば、Lodashには実装の1つがあります--lodash.com/docs#debounce
shershen

これは、ベーコンやRxのようなものが役立つときです。
elclanrs 2015年

@shershenはい-我々はこれを持っているが、これは、私は約束をキャンセルできるようにしたいので...少し時間がかかる場合がありますので、多くのUI issue..theサーバークエリについてではありません
ムーンウォーカー


RxjsのObservablesをお試しください
FieryCod

回答:


173

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

ES6の約束はまだキャンセルをサポートしていません。それはその途上にあり、そのデザインは多くの人々が本当に一生懸命取り組んだものです。サウンドキャンセルのセマンティクスを正しく理解するのは困難であり、これは進行中の作業です。「フェッチ」レポ、エスディスカス、およびGHに関する他のいくつかのレポについては興味深い議論がありますが、私があなただったら辛抱強く待ってください。

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

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

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

ええ、それについては申し訳ありません。彼らはのようないくつかの有用なものなしに行ってきましたので、 -約束はさらに、物事が指定された前に、最初に取得しなければならなかった.finally.cancel-それはDOMを通じてスペックに、かかわらず、その途中にあります。キャンセルは後から考えたものではなく、時間の制約であり、API設計に対するより反復的なアプローチです。

じゃあどうすればいい?

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

  • ブルーバードのようなサードパーティのライブラリを使用してください。このライブラリは、仕様よりもはるかに高速に移動できるため、キャンセルやその他の多くの機能があります。これは、WhatsAppのような大企業が行っていることです。
  • キャンセルトークンを渡します

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

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でオブザーバブルがネイティブになるのを待ち、見るのを待つと思います。しかし、それら先行入力に気の利いたものです。


28
ベンジャミン、あなたの答えを読んで本当に楽しかったです。非常によく考えられ、構造化され、明確であり、優れた実用的な例と代替案があります。本当に役に立ちました。ありがとうございました。
ムーンウォーカー2015年

@FranciscoPresenciaキャンセルトークンは、ステージ1の提案として進行中です。
Benjamin Gruenbaum 2016

このトークンベースのキャンセルについてはどこで確認できますか?提案はどこにありますか?
2017

@harm提案は、ステージ1で死んでいる
ベンジャミンGruenbaum

1
私はロンの作品が大好きですが、人々がまだ使っていない図書館の推薦をする前に少し待つべきだと思います:]リンクをありがとう、私はそれをチェックします!
ベンジャミングルエンバウム

24

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

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

別の約束は素晴らしいトークンを作り、キャンセルを簡単に実装できるようにし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、引数としての約束をキャンセル取るためだった、それがネットワークアクティビティを取り消すことができます。

私がしまし提案し、この上の「約束パターンキャンセル」まさにその示唆するように、ESを-話し合うfetchこれを行います。


@jibなぜ私の変更を拒否するのですか?私はそれを明確にします。
allenyllee

8

私はMozillaJSリファレンスをチェックして、これを見つけました:

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とp2Promise.race(...)を引数として入力しました。これにより、実際には新しい解決の約束が作成されます。これは、必要なものです。


ニース-これはおそらくまさに私が必要としているものです。やってみます。
ムーンウォーカー2015年

問題がある場合は、ここにコードを貼り付けて、サポートさせてください:)
nikola-miljkovic 2015年

6
それを試してみました。そこにはありません。この解決さ最速の約束は...私はいつも..任意の古い約束をキャンセルすなわち無条件に提出し、最新のを解決する必要があり
ムーンウォーカー

1
このようにして、他のすべての約束が処理されなくなり、実際に約束をキャンセルすることはできません。
nikola-miljkovic 2015年

私はそれを試しました、2番目の約束(この例の1つ)はプロセスを終了させません:(
mortezaatai​​y20年

3

Node.jsとElectronの場合、JavaScript用のPromise Extensions(Prex)を使用することを強くお勧めします。その著者ロンBucktonはキー活字体のエンジニアの一つであり、また、現在のTC39の背後にある男であるECMAScriptのキャンセル提案。ライブラリは十分に文書化されており、Prexの一部が標準に準拠する可能性があります。

個人的にとC#の背景から来て、私は非常にPREXは、既存の時にモデル化されているという事実が好きマネージドスレッドでキャンセルすなわちで撮影したアプローチに基づいて、フレームワーク、CancellationTokenSource/ CancellationToken.NET APIを。私の経験では、これらはマネージドアプリに堅牢なキャンセルロジックを実装するのに非常に便利です。

また、Browserifyを使用してPrexをバンドルすることにより、ブラウザー内で機能することを確認しました

ここで(キャンセルと遅延の一例で要点は、runkit使用して、PREXをそのためCancellationTokenDeferred)。

// by @noseratio
// https://gist.github.com/noseratio/141a2df292b108ec4c147db4530379d2
// https://runkit.com/noseratio/cancellablepromise

const prex = require('prex');

/**
 * A cancellable promise.
 * @extends Promise
 */
class CancellablePromise extends Promise {
  static get [Symbol.species]() { 
    // tinyurl.com/promise-constructor
    return Promise; 
  }

  constructor(executor, token) {
    const withCancellation = async () => {
      // create a new linked token source 
      const linkedSource = new prex.CancellationTokenSource(token? [token]: []);
      try {
        const linkedToken = linkedSource.token;
        const deferred = new prex.Deferred();
  
        linkedToken.register(() => deferred.reject(new prex.CancelError()));
  
        executor({ 
          resolve: value => deferred.resolve(value),
          reject: error => deferred.reject(error),
          token: linkedToken
        });

        await deferred.promise;
      } 
      finally {
        // this will also free all linkedToken registrations,
        // so the executor doesn't have to worry about it
        linkedSource.close();
      }
    };

    super((resolve, reject) => withCancellation().then(resolve, reject));
  }
}

/**
 * A cancellable delay.
 * @extends Promise
 */
class Delay extends CancellablePromise {
  static get [Symbol.species]() { return Promise; }

  constructor(delayMs, token) {
    super(r => {
      const id = setTimeout(r.resolve, delayMs);
      r.token.register(() => clearTimeout(id));
    }, token);
  }
}

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

  let delay = 1000;
  console.log(`delaying by ${delay}ms`); 
  await new Delay(delay, token);
  console.log("successfully delayed."); // we should reach here

  delay = 2000;
  console.log(`delaying by ${delay}ms`); 
  await new Delay(delay, token);
  console.log("successfully delayed."); // we should not reach here
}

main().catch(error => console.error(`Error caught, ${error}`));

キャンセルは人種であることに注意してください。つまり、プロミスは正常に解決された可能性がありますが、それを観察するまでに(awaitまたはでthen)、キャンセルもトリガーされた可能性があります。このレースをどのように処理するかはあなた次第ですがtoken.throwIfCancellationRequested()、上記のように余分な時間を呼び出すことは問題ありません。


1

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

私は(ネットワーククライアントではなく)Promiseベースのクライアントを持っていて、UIをスムーズに保つために、常に最新の要求されたデータをユーザーに提供したいと考えていました。

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

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


スロムスキーの質問は、UIに何を表示するかではありません。その程度キャンセル約束
Cyber​​Abhay


0

終了する前に、約束を拒否することができます。

// Our function to cancel promises receives a promise and return the same one and a cancel function
const cancellablePromise = (promiseToCancel) => {
  let cancel
  const promise = new Promise((resolve, reject) => {
    cancel = reject
    promiseToCancel
      .then(resolve)
      .catch(reject)
  })
  return {promise, cancel}
}

// A simple promise to exeute a function with a delay
const waitAndExecute = (time, functionToExecute) => new Promise((resolve, reject) => {
  timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    resolve(functionToExecute())
  }, timeInMs)
})

// The promise that we will cancel
const fetchURL = () => fetch('https://pokeapi.co/api/v2/pokemon/ditto/')

// Create a function that resolve in 1 seconds. (We will cancel it in 0.5 secs)
const {promise, cancel} = cancellablePromise(waitAndExecute(1, fetchURL))

promise
  .then((res) => {
    console.log('then', res) // This will executed in 1 second
  })
  .catch(() => {
    console.log('catch') // We will force the promise reject in 0.5 seconds
  })

waitAndExecute(0.5, cancel) // Cancel previous promise in 0.5 seconds, so it will be rejected before finishing. Commenting this line will make the promise resolve

残念ながら、フェッチ呼び出しはすでに実行されているため、[ネットワーク]タブに呼び出しが解決されていることがわかります。あなたのコードはそれを無視します。


0

外部パッケージによって提供されるPromiseサブクラスを使用して、これは次のように実行できます。ライブデモ

import CPromise from "c-promise2";

function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
    return new CPromise((resolve, reject, {signal}) => {
        fetch(url, {...fetchOptions, signal}).then(resolve, reject)
    }, timeout)
}

const chain= fetchWithTimeout('http://localhost/')
    .then(response => response.json())
    .then(console.log, console.warn);

//chain.cancel(); call this to abort the promise and releated request

-1

@jibは私の変更を拒否するので、ここに回答を投稿します。これは、@ jibの回答をいくつかコメント付きで変更し、より理解しやすい変数名を使用しているだけです。

以下に、2つの異なるメソッドの例を示します。1つはresolve()で、もう1つはreject()です。

let cancelCallback = () => {};

input.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancelCallback(); //cancel previous promise by calling cancelCallback()

  let setCancelCallbackPromise = () => {
    return new Promise((resolve, reject) => {
      // set cancelCallback when running this promise
      cancelCallback = () => {
        // pass cancel messages by resolve()
        return resolve('Canceled');
      };
    })
  }

  Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
    // check if the calling of resolve() is from cancelCallback() or getSearchResults()
    if (results == 'Canceled') {
      console.log("error(by resolve): ", results);
    } else {
      console.log(`results for "${term}"`, results);
    }
  });
}


input2.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancelCallback(); //cancel previous promise by calling cancelCallback()

  let setCancelCallbackPromise = () => {
    return new Promise((resolve, reject) => {
      // set cancelCallback when running this promise
      cancelCallback = () => {
        // pass cancel messages by reject()
        return reject('Canceled');
      };
    })
  }

  Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
    // check if the calling of resolve() is from cancelCallback() or getSearchResults()
    if (results !== 'Canceled') {
      console.log(`results for "${term}"`, results);
    }
  }).catch(error => {
    console.log("error(by reject): ", error);
  })
}

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.