取消香草ECMAScript 6 Promise链


110

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

我已经在QUnit之上编写了一个JavaScript测试框架。该框架通过在.NET中运行每个测试来同步运行测试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()s 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的属性(而是-它是返回它的函数的属性),如果您有兴趣,我可以举一个简短的例子。
本杰明·格伦鲍姆

@BenjaminGruenbaum我知道已经快一年了,但是如果您有时间写一个例子,我仍然很感兴趣。:)
dx_over_dt '16

1
它已经在一年前,但它的前两天昨天取消标记和撤销的承诺,移动到第1阶段已正式讨论
本杰明Gruenbaum

3
ES6取消承诺的答案是可以观察到的。您可以在这里阅读有关此内容的更多信息:github.com/Reactive-Extensions/RxJS
Frank Goortani

链接关于使用Prex库的承诺取消的答案
noseratio '18

Answers:


75

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

否。至少在ECMAScript 6中没有。then默认情况下,不幸的是,承诺(及其处理程序)是无法取消的。关于电子讨论有一些讨论(例如,这里),涉及到如何以正确的方式进行此操作,但是无论哪种方法获胜,都不会在ES6中实现。

当前的观点是,子类化将允许使用您自己的实现来创建可取消的Promise (不确定该如何工作)

在语言委托人找到最佳方法之前(希望是ES7?),您仍然可以使用userland Promise实现,其中许多功能都取消了。

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


2
“一点讨论”-我可以链接到esdiscuss或GitHub上的30个线程:)(更不用说您自己的帮助,帮助取消bluebird 3.0)
Benjamin Gruenbaum 2015年

@BenjaminGruenbaum:您是否准备好在某些地方共享这些链接?长期以来,我一直想总结观点和尝试,并向esdiscuss发表建议,因此,如果我能回过头,我什么都没忘记,我会很高兴。
Bergi 2015年

我可以方便地在工作中使用它们-因此我会在3-4天之内得到它们。您可以在promise-aplus下查看promise取消规范,以获取良好的开端。
Benjamin Gruenbaum

1
@ LUH3417:“正常”功能在这方面很无聊。您启动一个程序并等待其完成-否则您kill就忽略了可能在怪异状态下产生的副作用,从而使您的环境无法进入(因此,通常也将其丢弃,例如,任何未完成的输出)。但是,非阻塞或异步功能是为在交互式应用程序中运行而构建的,在交互式应用程序中,您希望对正在进行的操作的执行具有这种更好的控制。
Bergi 2016年

6
多梅尼克(Domenic)删除了TC39提案... ... cc @BenjaminGruenbaum
塞尔吉奥(Sergio)

50

尽管在ES6中没有标准的方法可以执行此操作,但是有一个名为Bluebird的库来处理此问题。

在react文档中也有推荐的方法。它看起来与您在第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
这个取消的定义只是拒绝了诺言。它取决于“取消”的定义。
亚历山大·米尔斯

1
如果您要取消承诺,会发生什么?
马修·布鲁彻

1
这种方法的问题是,如果您有一个永远不会解决或拒绝的承诺,那么它永远不会被取消。
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 named'); resolve()},10000)});; ```
shmck

2
问题是如何取消promise(=>停止then执行的链),而不是如何取消setTimeout(=> clearTimeout)或同步代码,除非您在每行(if (canceled) return)之后放置if,否则将无法实现。(不要这样做)
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

值得注意的是,无法在承诺中暂停执行的原因是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,然后promise将绕过所有next块,然后直接进入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>

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

现场例子

编辑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')的promise。

在解决,拒绝或on_cancel之前,禁止无缘无故地调用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-abortablehttps : //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();
});


0

如果要停止执行所有then / catch,可以通过注入永远无法解决的承诺来实现。它可能会发生内存泄漏,但可以解决此问题,并且在大多数应用程序中不会造成过多的内存浪费。

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()提早退出。这非常有效,尤其是在Web Workers中,现有的微任务已在onmessage处理程序中的Promise中排队。

// 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()链接起来以处理拒绝处理,这是我对@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

我仍在研究这个想法,但是这是我如何使用以下方法实现可取消的Promise setTimeout以示例为例,。

这个想法是,只要您确定了承诺,便会解决或拒绝它,因此应该确定要取消的时间,满足条件,然后reject()自己调用该函数。

  • 首先,我认为有两个原因可以提早完成承诺:将其兑现并完成(我称之为resolve)并取消(我称之为reject)。当然,那只是我的感觉。当然有一个Promise.resolve()方法,但是它在构造函数中,并返回一个虚拟的已解决的promise。该实例resolve()方法实际上解析了实例化的Promise对象。

  • 其次,您可以在返回新对象之前愉快地将其添加到新创建的Promise对象中,因此,我刚刚添加了resolve()reject()使它独立的方法。

  • 第三,诀窍是以后可以访问执行程序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>

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.