Javascript 取消一个普通的 ECMAScript 6 Promise 链

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/29478751/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 03:26:00  来源:igfitidea点击:

Cancel a vanilla ECMAScript 6 Promise chain

javascriptpromisecancellationes6-promise

提问by dx_over_dt

Is there a method for clearing the .thens of a JavaScript Promiseinstance?

是否有清除.thenJavaScriptPromise实例的s 的方法?

I've written a JavaScript test framework on top of QUnit. The framework runs tests synchronously by running each one in a Promise. (Sorry for the length of this code block. I commented it as best I can, so it feels less tedious.)

我已经在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);
        });
    });
}

If a test times out, my timeout Promise will assert.fail()on the test so that the test is marked as failed, which is all well and good, but the test continues to run because the test Promise (result) is still waiting to resolve it.

如果测试超时,我的超时 Promise 将assert.fail()在测试中,以便将测试标记为失败,这一切都很好,但测试继续运行,因为测试 Promise ( result) 仍在等待解决它。

I need a good way to cancel my test. I can do it by creating a field on the framework module this.cancelTestor something, and checking every so often (e.g. at the beginning of each then()iteration) within the test whether to cancel out. However, ideally, I could use $$(at).on("timeout", /* something here */)to clear the remaining then()s on my resultvariable, so that none of the rest of the test is run.

我需要一个好的方法来取消我的测试。我可以通过在框架模块this.cancelTest或其他东西上创建一个字段来做到这一点,并在测试中经常检查(例如在每次then()迭代开始时)是否取消。但是,理想情况下,我可以$$(at).on("timeout", /* something here */)用来清除变量then()上剩余的s result,这样其余的测试都不会运行。

Does something like this exist?

这样的东西存在吗?

Quick Update

快速更新

I tried using Promise.race([result, at.promise]). It didn't work.

我尝试使用Promise.race([result, at.promise]). 它没有用。

Update 2 + confusion

更新 2 + 混乱

To unblock me, I added a few lines with the mod.cancelTest/polling within the test idea. (I also removed the event trigger.)

为了解除对我的阻止,我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);
});

I set a breakpoint in the catchstatement, and it's being hit. What's confusing me now is that the then()statement isn't being called. Ideas?

我在catch语句中设置了一个断点,它被命中了。现在让我感到困惑的是,then()没有调用该语句。想法?

Update 3

更新 3

Figured the last thing out. fn.call()was throwing an error which I didn't catch, so the test promise was rejecting before at.promise.catch()could resolve it.

想通了最后一件事。 fn.call()抛出一个我没有发现的错误,所以测试承诺在at.promise.catch()可以解决它之前被拒绝。

采纳答案by Bergi

Is there a method for clearing the .thens of a JavaScript Promise instance?

是否有清除.thenJavaScript Promise 实例的s 的方法?

No. Not in ECMAScript 6 at least. Promises (and their thenhandlers) are uncancellable by default (unfortunately). There is a bit of discussion on es-discuss (e.g. here) about how to do this in the right way, but whatever approach will win it won't land in ES6.

不。至少在 ECMAScript 6 中没有。承诺(及其then处理程序)默认是不可取消的(不幸的是)。在 es-discuss(例如这里)上有一些关于如何以正确的方式做到这一点的讨论,但无论哪种方法会获胜,它都不会出现在 ES6 中。

The current standpoint is that subclassing will allow to create cancellable promises using your own implementation (not sure how well that'll work).

当前的观点是,子类化将允许使用您自己的实现来创建可取消的承诺(不确定它的效果如何)

Until the language commitee has figured out the best way (ES7 hopefully?)you can still use userland Promise implementations, many of which feature cancellation.

在语言委员会找到最好的方法之前(希望是 ES7?),您仍然可以使用用户空间 Promise 实现,其中许多功能取消。

Current discussion is in the https://github.com/domenic/cancelable-promiseand https://github.com/bergus/promise-cancellationdrafts.

当前讨论在https://github.com/domenic/cancelable-promisehttps://github.com/bergus/promise-cancellation草案中。

回答by Michael Yagudaev

While there isn't a standard way of doing this in ES6, there is a library called Bluebirdto handle this.

虽然在 ES6 中没有标准的方法来做到这一点,但有一个名为Bluebird的库来处理这个问题。

There is also a recommended way described as part of the react documentation. It looks similar to what you have in your 2 and 3rd updates.

反应文档中还描述了一种推荐的方法。它看起来与您在第 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

Taken from: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

摘自:https: //facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

回答by Pho3nixHun

I'm really surprised that no-one mentions Promise.raceas a candidate for this:

我真的很惊讶没有人提到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 });

回答by Slava M

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;
};

Usage:

用法:

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

回答by WebBrother

There are a few npm libraries for cancellable promises.

有一些用于可取消承诺的 npm 库。

  1. p-cancelablehttps://github.com/sindresorhus/p-cancelable

  2. cancelable-promisehttps://github.com/alkemics/CancelablePromise

  1. p-cancelable https://github.com/sindresorhus/p-cancelable

  2. 可取消承诺https://github.com/alkemics/CancelablePromise

回答by nikksan

It is actually impossible to stop the execution of the promise, but you can hiHyman the reject and call it from the promise itself.

阻止promise的执行实际上是不可能的,但是你可以劫持reject并从promise本身调用它。

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

    return cancelablePromise;
  }
}

Usage:

用法:

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

p.catch(console.log);

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

回答by Sohail

Promise can be cancelled with the help of AbortController.

可以在 的帮助下取消 Promise AbortController

Is there a method for clearing then:yes you can reject the promise with AbortControllerobject and then the promisewill bypass all then blocks and go directly to the catch block.

那么是否有清除方法:是的,您可以拒绝带有AbortController对象的承诺,然后promise将绕过所有 then 块并直接转到 catch 块。

Example:

例子:

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

html


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

Note: need to add polyfill, not supported in all browser.

注意:需要添加 polyfill,并非所有浏览器都支持。

Live Example

现场示例

Edit elegant-lake-5jnh3

编辑优雅湖5jnh3

回答by vlio20

If your code is placed in a class you could use a decorator for that. You have such decorator in the utils-decorators(npm install --save utils-decorators). It will cancel the previous invocation of the decorated method if before the resolving of the previous call there was made another call for that specific method.

如果您的代码放在一个类中,您可以为此使用装饰器。您在utils-decorators( npm install --save utils-decorators) 中有这样的装饰。如果在解析前一个调用之前对该特定方法进行了另一个调用,它将取消对装饰方法的前一个调用。

import {cancelPrevious} from 'utils-decorators';

class SomeService {

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

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

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

回答by Devi

Try promise-abortable: https://www.npmjs.com/package/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();
});

回答by Shimon Doodkin

simple version:

简单版本

just give out the reject function.

只需给出拒绝功能。

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();} 
 })
}

a wraper solution (factory)

包装解决方案(工厂)

the solution I found is to pass a cancel_holder object. it will have a cancel function. if it has a cancel function then it is cancelable.

我找到的解决方案是传递一个 cancel_holder 对象。它将具有取消功能。如果它有取消功能,那么它是可取消的。

This cancel function rejects the promise with Error('canceled').

这个取消函数用 Error('canceled') 拒绝承诺。

Before resolve, reject, or on_cancel prevent the cancel function to be called without reason.

在 resolve、reject 或 on_cancel 之前,会阻止无故调用取消函数。

I have found convenient to pass the cancel action by injection

我发现通过注入传递取消操作很方便

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')
})