バニラECMAScript 6プロミスチェーンをキャンセルする


110

.thenJavaScript Promiseインスタンスのs をクリアする方法はありますか?

QUnitの上にJavaScriptテストフレームワークを記述しました。フレームワークは、それぞれでテストを同期的に実行しPromiseます。(このコードブロックの長さで申し訳ありません。できる限りコメントを付けたので、退屈さが少なく感じられます。)

/* Promise extension -- used for easily making an async step with a
       timeout without the Promise knowing anything about the function 
       it's waiting on */
$$.extend(Promise, {
    asyncTimeout: function (timeToLive, errorMessage) {
        var error = new Error(errorMessage || "Operation timed out.");
        var res, // resolve()
            rej, // reject()
            t,   // timeout instance
            rst, // reset timeout function
            p,   // the promise instance
            at;  // the returned asyncTimeout instance

        function createTimeout(reject, tempTtl) {
            return setTimeout(function () {
                // triggers a timeout event on the asyncTimeout object so that,
                // if we want, we can do stuff outside of a .catch() block
                // (may not be needed?)
                $$(at).trigger("timeout");

                reject(error);
            }, tempTtl || timeToLive);
        }

        p = new Promise(function (resolve, reject) {
            if (timeToLive != -1) {
                t = createTimeout(reject);

                // reset function -- allows a one-time timeout different
                //    from the one original specified
                rst = function (tempTtl) {
                    clearTimeout(t);
                    t = createTimeout(reject, tempTtl);
                }
            } else {
                // timeToLive = -1 -- allow this promise to run indefinitely
                // used while debugging
                t = 0;
                rst = function () { return; };
            }

            res = function () {
                clearTimeout(t);
                resolve();
            };

            rej = reject;
        });

        return at = {
            promise: p,
            resolve: res,
            reject: rej,
            reset: rst,
            timeout: t
        };
    }
});

/* framework module members... */

test: function (name, fn, options) {
    var mod = this; // local reference to framework module since promises
                    // run code under the window object

    var defaultOptions = {
        // default max running time is 5 seconds
        timeout: 5000
    }

    options = $$.extend({}, defaultOptions, options);

    // remove timeout when debugging is enabled
    options.timeout = mod.debugging ? -1 : options.timeout;

    // call to QUnit.test()
    test(name, function (assert) {
        // tell QUnit this is an async test so it doesn't run other tests
        // until done() is called
        var done = assert.async();
        return new Promise(function (resolve, reject) {
            console.log("Beginning: " + name);

            var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
            $$(at).one("timeout", function () {
                // assert.fail() is just an extension I made that literally calls
                // assert.ok(false, msg);
                assert.fail("Test timed out");
            });

            // run test function
            var result = fn.call(mod, assert, at.reset);

            // if the test returns a Promise, resolve it before resolving the test promise
            if (result && result.constructor === Promise) {
                // catch unhandled errors thrown by the test so future tests will run
                result.catch(function (error) {
                    var msg = "Unhandled error occurred."
                    if (error) {
                        msg = error.message + "\n" + error.stack;
                    }

                    assert.fail(msg);
                }).then(function () {
                    // resolve the timeout Promise
                    at.resolve();
                    resolve();
                });
            } else {
                // if test does not return a Promise, simply clear the timeout
                // and resolve our test Promise
                at.resolve();
                resolve();
            }
        }).then(function () {
            // tell QUnit that the test is over so that it can clean up and start the next test
            done();
            console.log("Ending: " + name);
        });
    });
}

テストがタイムアウトした場合、タイムアウトPromiseがassert.fail()テストを実行し、テストが失敗したとマークされます。これは問題ありませんが、テストPromise(result)が解決を待機しているため、テストは引き続き実行されます。

テストをキャンセルする良い方法が必要です。フレームワークモジュールthis.cancelTestなどにフィールドを作成し、then()キャンセルするかどうかをテスト内で頻繁に(たとえば、各反復の開始時に)チェックすることで実現できます。ただし、理想的には、変数の$$(at).on("timeout", /* something here */)残りthen()のをクリアして、残りのresultテストが実行されないようにすることができます。

このようなものは存在しますか?

クイックアップデート

使ってみましたPromise.race([result, at.promise])。うまくいきませんでした。

アップデート2 +混乱

ブロックを解除するためmod.cancelTestに、テストアイデア内に/ pollingを含む数行を追加しました。(イベントトリガーも削除しました。)

return new Promise(function (resolve, reject) {
    console.log("Beginning: " + name);

    var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
    at.promise.catch(function () {
        // end the test if it times out
        mod.cancelTest = true;
        assert.fail("Test timed out");
        resolve();
    });

    // ...
    
}).then(function () {
    // tell QUnit that the test is over so that it can clean up and start the next test
    done();
    console.log("Ending: " + name);
});

catchステートメントにブレークポイントを設定しましたが、ヒットしました。私を混乱させているのは、then()ステートメントが呼び出されていないことです。アイデア?

アップデート3

最後のものを考え出した。 fn.call()私がキャッチしなかったエラーを投げていたので、テストプロミスはそれat.promise.catch()を解決する前に拒否していました。


ES6のpromiseでキャンセルを行うことは可能ですが、それはpromiseのプロパティではありません(むしろ、それはそれを返す関数のプロパティです)。
Benjamin Gruenbaum 2015

@BenjaminGruenbaum私はそれがほぼ1年になったことを知っていますが、例を書く時間があれば、私はまだ興味があります。:)
dx_over_dt 2016年

1
これは、前年同期をされているが、それは正式に2日間のキャンセルトークンと解約の約束で、昨日はステージ1に移動する前に議論されてきた
ベンジャミンGruenbaum

3
Promiseのキャンセルに対するES6の答えは注目に値します。詳細については、こちらをご覧ください:github.com/Reactive-Extensions/RxJS
Frank Goortani 2017年

約束をキャンセルするためのライブラリの使用に関する私の答えをリンクしPrexます。
noseratio

回答:


75

.thenJavaScript Promiseインスタンスのs をクリアする方法はありますか?

いいえ、少なくともECMAScript 6にはありません。プロミス(およびそのthenハンドラー)は、デフォルトでは(残念ながら)キャンセルできません。これを正しい方法で行う方法についてes-discuss(例:ここ)で少し議論がありますが、どのアプローチを採用しても、ES6には到達しません。

現在の見解では、サブクラス化により、独自の実装を使用してキャンセル可能なプロミスを作成できるようになります(それがどれほどうまく機能するかわかりません)

言語委員会が最良の方法を見つけるまで(ES7がうまくいけば?)、ユーザーランドのPromise実装を引き続き使用できます。その多くは機能のキャンセルです。

現在の議論はhttps://github.com/domenic/cancelable-promiseおよびhttps://github.com/bergus/promise-cancellationドラフトで行われています


2
「少しのディスカッション」-私はesdiscussまたはGitHubでおそらく30スレッドにリンクできます(bluebird 3.0でのキャンセルに関するあなた自身のヘルプは言うまでもありません)
Benjamin Gruenbaum

@BenjaminGruenbaum:どこかで共有できるリンクはありますか?私は長い間、意見や試みを要約して提案をesdiscussに投稿したかったので、何も忘れていないことを確認できたら嬉しいです。
Bergi

私はそれらを仕事で便利に持っています-それで私は3-4日でそれらを持っています。あなたはpromise-aplusの下でpromiseキャンセル仕様をチェックして良いスタートを切ることができます。
Benjamin Gruenbaum 2015

1
@ LUH3417:「通常の」関数はその点で退屈です。あなたはプログラムを開始し、それが終了するまで待つ-あるいはあなたはkillそれがあなたの環境に残された副作用がおそらく変な状態で無視します(そのため、通常、それだけでなく、たとえば、すべての半完成した出力も捨てます)。ただし、ノンブロッキング関数または非同期関数は、進行中の操作の実行をそのような種類の細かい制御にしたい対話型アプリケーションで機能するように構築されています。
Bergi

6
Domenic がTC39プロポーザルを削除しました... ... cc @BenjaminGruenbaum
Sergio

50

ES6でこれを行う標準的な方法はありませんが、これを処理するBluebirdと呼ばれるライブラリがあります。

反応ドキュメントの一部として説明されている推奨される方法もあります。2回目と3回目のアップデートにあるものと似ています。

const makeCancelable = (promise) => {
  let hasCanceled_ = false;

  const wrappedPromise = new Promise((resolve, reject) => {
    promise.then((val) =>
      hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
    );
    promise.catch((error) =>
      hasCanceled_ ? reject({isCanceled: true}) : reject(error)
    );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled_ = true;
    },
  };
};

const cancelablePromise = makeCancelable(
  new Promise(r => component.setState({...}}))
);

cancelablePromise
  .promise
  .then(() => console.log('resolved'))
  .catch((reason) => console.log('isCanceled', reason.isCanceled));

cancelablePromise.cancel(); // Cancel the promise

取得元:https : //facebook.github.io/react/blog/2015/12/16/ismount-antipattern.html


1
このキャンセルの定義は、単に約束を拒否しているだけです。「キャンセル」の定義によって異なります。
Alexander Mills

1
そして、一連の約束をキャンセルしたい場合はどうなりますか?
Matthieu

1
このアプローチの問題は、解決も拒否もしないPromiseがある場合、キャンセルされないことです。
DaNeSh

2
これは部分的に正しいですが、プロミスチェーンが長い場合、このアプローチは機能しません。
Veikko Karsikko、

11

Promise.raceこれの候補として誰も言及していないことに本当に驚いています:

const actualPromise = new Promise((resolve, reject) => { setTimeout(resolve, 10000) });
let cancel;
const cancelPromise = new Promise((resolve, reject) => {
    cancel = reject.bind(null, { canceled: true })
})

const cancelablePromise = Object.assign(Promise.race([actualPromise, cancelPromise]), { cancel });

3
これがうまくいくとは思いません。ログ記録のプロミスを変更した場合cancel()でも、実行するとログが呼び出されます。`` `const actualPromise = new Promise((resolve、reject)=> {setTimeout(()=> {console.log( 'actual called'); resolve()}、10000)}); `` `
shmck

2
問題は、どのようにプロミスをキャンセルするか(=>チェーンthensの実行を停止する)で、キャンセルsetTimeout(=> clearTimeout)や同期コードをキャンセルする方法でif (canceled) returnはありませんでした。(これを行わないでください)
Pho3nixHun

10
const makeCancelable = promise => {
    let rejectFn;

    const wrappedPromise = new Promise((resolve, reject) => {
        rejectFn = reject;

        Promise.resolve(promise)
            .then(resolve)
            .catch(reject);
    });

    wrappedPromise.cancel = () => {
        rejectFn({ canceled: true });
    };

    return wrappedPromise;
};

使用法:

const cancelablePromise = makeCancelable(myPromise);
// ...
cancelablePromise.cancel();

5

実際にはプロミスの実行を停止することは不可能ですが、拒否をハイジャックしてプロミス自体から呼び出すことができます。

class CancelablePromise {
  constructor(executor) {
    let _reject = null;
    const cancelablePromise = new Promise((resolve, reject) => {
      _reject = reject;
      return executor(resolve, reject);
    });
    cancelablePromise.cancel = _reject;

    return cancelablePromise;
  }
}

使用法:

const p = new CancelablePromise((resolve, reject) => {
  setTimeout(() => {
    console.log('resolved!');
    resolve();
  }, 2000);
})

p.catch(console.log);

setTimeout(() => {
  p.cancel(new Error('Messed up!'));
}, 1000);

1
@dx_over_dtあなたの編集は素晴らしいコメントですが、編集ではありません。そのような実質的な編集は、OPの権限に任せてください(もちろん、投稿がCommunity Wikiとしてマークされている場合を除きます)。
TylerH

@TylerHでは、タイプミスなどを修正するための編集のポイントは何ですか?または、情報が古くなったら更新しますか?他のユーザーの投稿権限を編集する機能は初めてです。
dx_over_dt

@dx_over_dtうん、編集はタイプミス、文法エラーを修正して投稿を改善し、構文の強調表示を追加することです(誰かがコードの束を投稿しただけで、たとえばインデントしたり、「 `」でタグ付けしない場合など)。追加の説明や事物の理由/正当化などの実質的なコンテンツを追加することは、通常、回答を投稿した人の範囲です。あなたはコメントでそれを提案することは自由であり、OPはコメントを通知され、それからそれに応答することができます、または彼らはあなた自身の提案を投稿自体に組み込むことができます。
TylerH

@dx_over_dt例外は、投稿が「コミュニティWiki」とマークされていて、それが共同投稿(Wikipediaなど)として機能することを意図していること、または失礼/乱用、危険/有害なコンテンツ(たとえば、ウイルスを与えたり、逮捕したりする可能性のある提案やコードなど)、または健康記録、電話番号、クレジットカードなどの個人情報。自分で削除してください。
TylerH

Promise内で実行を停止できない理由は、JavaScriptがシングルスレッドであることです。promise関数が実行されている間、他には何も実行されていないため、実行の停止をトリガーするものはありません。
dx_over_dt


2

これが私たちの実装です https://github.com/permettez-moi-de-construire/cancellable-promise

のように使用

const {
  cancellablePromise,
  CancelToken,
  CancelError
} = require('@permettezmoideconstruire/cancellable-promise')

const cancelToken = new CancelToken()

const initialPromise = SOMETHING_ASYNC()
const wrappedPromise = cancellablePromise(initialPromise, cancelToken)


// Somewhere, cancel the promise...
cancelToken.cancel()


//Then catch it
wrappedPromise
.then((res) => {
  //Actual, usual fulfill
})
.catch((err) => {
  if(err instanceOf CancelError) {
    //Handle cancel error
  }

  //Handle actual, usual error
})

どれ:

  • Promise APIに触れない
  • 内部でさらにキャンセルしてみましょう catch通話
  • 他の提案や実装とは異なり、解決ずに拒否されたキャンセルに依存する

プルとコメント歓迎


2

プロミスはの助けを借りてキャンセルできますAbortController

クリアする方法はあり ますかはい、AbortControllerオブジェクトのpromiseを拒否すると、thenはpromiseすべてのthenブロックをバイパスし、catchブロックに直接移動します。

例:

import "abortcontroller-polyfill";

let controller = new window.AbortController();
let signal = controller.signal;
let elem = document.querySelector("#status")

let example = (signal) => {
    return new Promise((resolve, reject) => {
        let timeout = setTimeout(() => {
            elem.textContent = "Promise resolved";
            resolve("resolved")
        }, 2000);

        signal.addEventListener('abort', () => {
            elem.textContent = "Promise rejected";
            clearInterval(timeout);
            reject("Promise aborted")
        });
    });
}

function cancelPromise() {
    controller.abort()
    console.log(controller);
}

example(signal)
    .then(data => {
        console.log(data);
    })
    .catch(error => {
        console.log("Catch: ", error)
    });

document.getElementById('abort-btn').addEventListener('click', cancelPromise);

HTML


    <button type="button" id="abort-btn" onclick="abort()">Abort</button>
    <div id="status"> </div>

注:ポリフィルを追加する必要があります。一部のブラウザではサポートされていません。

実例

elegant-lake-5jnh3を編集します


1

シンプルなバージョン

リジェクト機能を与えるだけです。

function Sleep(ms,cancel_holder) {

 return new Promise(function(resolve,reject){
  var done=false; 
  var t=setTimeout(function(){if(done)return;done=true;resolve();}, ms);
  cancel_holder.cancel=function(){if(done)return;done=true;if(t)clearTimeout(t);reject();} 
 })
}

ラッパーソリューション(工場)

私が見つけた解決策は、cancel_holderオブジェクトを渡すことです。キャンセル機能があります。キャンセル機能がある場合はキャンセル可能です。

このキャンセル関数は、Error( 'canceled')でプロミスを拒否します。

resolve、reject、またはon_cancelの前に、キャンセル関数が理由なく呼び出されるのを防ぎます。

キャンセルアクションをインジェクションで渡すと便利です

function cancelablePromise(cancel_holder,promise_fn,optional_external_cancel) {
  if(!cancel_holder)cancel_holder={};
  return new Promise( function(resolve,reject) {
    var canceled=false;
    var resolve2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; resolve.apply(this,arguments);}
    var reject2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; reject.apply(this,arguments);}
    var on_cancel={}
    cancel_holder.cancel=function(){
      if(canceled) return; canceled=true;

      delete cancel_holder.cancel;
      cancel_holder.canceled=true;

      if(on_cancel.cancel)on_cancel.cancel();
      if(optional_external_cancel)optional_external_cancel();

      reject(new Error('canceled'));
    };

    return promise_fn.call(this,resolve2,reject2,on_cancel);        
  });
}

function Sleep(ms,cancel_holder) {

 return cancelablePromise(cancel_holder,function(resolve,reject,oncacnel){

  var t=setTimeout(resolve, ms);
  oncacnel.cancel=function(){if(t)clearTimeout(t);}     

 })
}


let cancel_holder={};

// meanwhile in another place it can be canceled
setTimeout(function(){  if(cancel_holder.cancel)cancel_holder.cancel(); },500) 

Sleep(1000,cancel_holder).then(function() {
 console.log('sleept well');
}, function(e) {
 if(e.message!=='canceled') throw e;
 console.log('sleep interrupted')
})

1

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

$ npm install promise-abortable
import AbortablePromise from "promise-abortable";

const timeout = new AbortablePromise((resolve, reject, signal) => {
  setTimeout(reject, timeToLive, error);
  signal.onabort = resolve;
});

Promise.resolve(fn()).then(() => {
  timeout.abort();
});

1

コードがクラスに配置されている場合は、デコレーターを使用できます。utils-decoratorsnpm install --save utils-decorators)にそのようなデコレータがあります。前の呼び出しを解決する前に、その特定のメソッドに対して別の呼び出しが行われた場合、装飾されたメソッドの前の呼び出しをキャンセルします。

import {cancelPrevious} from 'utils-decorators';

class SomeService {

   @cancelPrevious()
   doSomeAsync(): Promise<any> {
    ....
   }
}

https://github.com/vlio20/utils-decorators#cancelprevious-method


0

すべてのthens / catchの実行を停止したい場合は、解決しないpromiseを注入することでこれを行うことができます。おそらくメモリリークの再処理がありますが、問題は修正され、ほとんどのアプリケーションでメモリの浪費が多すぎないはずです。

new Promise((resolve, reject) => {
    console.log('first chain link executed')
    resolve('daniel');
}).then(name => {
    console.log('second chain link executed')
    if (name === 'daniel') {
        // I don't want to continue the chain, return a new promise
        // that never calls its resolve function
        return new Promise((resolve, reject) => {
            console.log('unresolved promise executed')
        });
    }
}).then(() => console.log('last chain link executed'))

// VM492:2 first chain link executed
// VM492:5 second chain link executed
// VM492:8 unresolved promise executed

0

Promiseの「キャンセルされた」プロパティを設定して、シグナルを送信しthen()catch()早期に終了します。特に、onmessageハンドラーからPromisesにキューイングされている既存のマイクロタスクがあるWebワーカーでは非常に効果的です。

// Queue task to resolve Promise after the end of this script
const promise = new Promise(resolve => setTimeout(resolve))

promise.then(_ => {
  if (promise.canceled) {
    log('Promise cancelled.  Exiting early...');
    return;
  }

  log('No cancelation signaled.  Continue...');
})

promise.canceled = true;

function log(msg) {
  document.body.innerHTML = msg;
}


0

@Michael Yagudaevの答えは私には有効です。

しかし、元の答えは、拒否された処理を処理するために.catch()でラップされたpromiseをチェーンしていませんでした。これが、@ Michael Yagudaevの答えに加えて私の改善点です。

const makeCancelablePromise = promise => {
  let hasCanceled = false;
  const wrappedPromise = new Promise((resolve, reject) => {
    promise
      .then(val => (hasCanceled ? reject({ isCanceled: true }) : resolve(val)))
      .catch(
        error => (hasCanceled ? reject({ isCanceled: true }) : reject(error))
      );
  });

  return {
    promise: wrappedPromise,
    cancel() {
      hasCanceled = true;
    }
  };
};

// Example Usage:
const cancelablePromise = makeCancelable(
  new Promise((rs, rj) => {
    /*do something*/
  })
);
cancelablePromise.promise.then(() => console.log('resolved')).catch(err => {
  if (err.isCanceled) {
    console.log('Wrapped promise canceled');
    return;
  }
  console.log('Promise was not canceled but rejected due to errors: ', err);
});
cancelablePromise.cancel();

0

pがPromiseを含む変数のp.then(empty);場合、それが最終的に完了するか、すでに完了している場合(つまり、これは元の質問ではないことはわかっていますが、それが私の質問です)、約束を却下する必要があります。「空」はfunction empty() {}です。私は初心者で、おそらく間違っていますが、これらの他の答えは複雑すぎるようです。約束は単純であることになっています。


0

私はまだこのアイデアに取り組んでいますが、これは私がキャンセル可能なプロミスをどのように実装したかです setTimeout例として示します。

約束は、決定したときはいつでも解決または拒否されるため、キャンセルするかどうかを判断し、基準を満たし、reject()関数を自分で呼び出すことです。

  • 最初に、約束を早期に完了させる2つの理由があると思います。それを乗り越えて(resolveと呼んだ)キャンセルすることと、cancel(rejectと呼んだ)をキャンセルすることです。もちろん、それは私の気持ちです。もちろんPromise.resolve()メソッドはありますが、それはコンストラクタ自体にあり、ダミーの解決済みプロミスを返します。このインスタンスresolve()メソッドは、インスタンス化されたpromiseオブジェクトを実際に解決します。

  • 2つ目は、新しく作成したpromiseオブジェクトに返す前に、好きなものを何でも喜んで追加できることです。そのため、を自己完結型にするメソッドresolve()reject()メソッドを追加しました。

  • 3つ目は、後でエグゼキューターresolvereject関数にアクセスできるようにすることです。そのため、クロージャー内から単純なオブジェクトに単純に格納しています。

解決策はシンプルだと思いますが、大きな問題は見当たりません。

function wait(delay) {
  var promise;
  var timeOut;
  var executor={};
  promise=new Promise(function(resolve,reject) {
    console.log(`Started`);
    executor={resolve,reject};  //  Store the resolve and reject methods
    timeOut=setTimeout(function(){
      console.log(`Timed Out`);
      resolve();
    },delay);
  });
  //  Implement your own resolve methods,
  //  then access the stored methods
      promise.reject=function() {
        console.log(`Cancelled`);
        clearTimeout(timeOut);
        executor.reject();
      };
      promise.resolve=function() {
        console.log(`Finished`);
        clearTimeout(timeOut);
        executor.resolve();
      };
  return promise;
}

var promise;
document.querySelector('button#start').onclick=()=>{
  promise=wait(5000);
  promise
  .then(()=>console.log('I have finished'))
  .catch(()=>console.log('or not'));
};
document.querySelector('button#cancel').onclick=()=>{ promise.reject(); }
document.querySelector('button#finish').onclick=()=>{ promise.resolve(); }
<button id="start">Start</button>
<button id="cancel">Cancel</button>
<button id="finish">Finish</button>

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