取消一个纯ECMAScript 6 Promise链。

158

有没有一种方法可以清除JavaScript Promise实例的 .then 方法?

我在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 */)来清除剩余的result变量上的所有then(),以便不运行测试的其余部分。
有像这样的东西存在吗?
快速更新
我尝试使用Promise.race([result, at.promise])。 它没有起作用。
更新2 + 混淆
为了解除阻塞,我添加了一些具有mod.cancelTest/轮询测试思路的行。 (我还删除了事件触发器。)
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()抛出了一个错误,我没有捕获到,因此测试 Promise 在at.promise.catch()处理之前就被拒绝了。


可以使用ES6 Promise进行取消操作,但这不是Promise的属性(而是返回它的函数的属性)。如果您感兴趣,我可以举一个简短的例子。 - Benjamin Gruenbaum
@BenjaminGruenbaum 我知道已经快一年了,但如果你有时间写一个例子,我仍然很感兴趣。 :) - dx_over_dt
1
一年前就已经开始讨论取消令牌和可取消的承诺移动到第一阶段,但直到前天才正式宣布。 - Benjamin Gruenbaum
5
ES6 对取消 Promise 的解决方案是 Observable。您可以在此处阅读更多信息:https://github.com/Reactive-Extensions/RxJS - Frank Goortani
将我在Stack Overflow上关于使用Prex库进行Promise取消的回答链接(https://dev59.com/C10a5IYBdhLWcg3whJDW#53093799)添加到文章中。 - noseratio - open to work
17个回答

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
我仍在思考这个想法,但以下是使用setTimeout作为示例实现可取消Promise的方法。
这个想法是,无论何时您决定解决或拒绝promise,都应该是一个决定何时取消、满足标准,然后自己调用reject()函数的问题。
  • 首先,我认为完成一个承诺有两个原因:完成它(我称之为resolve)和取消它(我称之为reject)。当然,这只是我的感觉。当然有一个Promise.resolve()方法,但它在构造函数本身中,并返回一个虚假的已解决的承诺。这个实例resolve()方法实际上解决了一个实例化的承诺对象。

  • 其次,在返回新创建的承诺对象之前,您可以随意添加任何内容,因此我只是添加了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>


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);应该在它最终完成或者已经完成时解除这个Promise(是的,我知道这不是原来的问题,但这是我的问题)。"empty"是function empty() {}。我只是一个初学者,可能是错的,但其他答案似乎太复杂了。Promise应该是简单的。

0

如果您的代码放在一个类中,您可以使用装饰器来实现。您可以在utils-decoratorsnpm install --save utils-decorators)中找到这样的装饰器。如果在解决上一个调用之前,有另一个调用该特定方法的调用,则它将取消该被装饰方法的前一个调用。

import {cancelPrevious} from 'utils-decorators';

class SomeService {

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

或者你可以使用一个包装函数:

import {cancelPreviousify} from 'utils-decorators';

const cancelable = cancelPreviousify(originalMethod)

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


-1
如果您想停止执行所有的then/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

1
导致内存泄漏的方法不应该是一个解决方案。 - ofarukcaki
谢谢您的意见。在回答中,负面方面已经被清楚地记录下来,如果有人认为这对他们的应用程序有价值,那就由他们自己决定吧。 - DanLatimer

-2

使用CPromise包,我们可以采用以下方法(演示

import CPromise from "c-promise2";

const chain = new CPromise((resolve, reject, { onCancel }) => {
  const timer = setTimeout(resolve, 1000, 123);
  onCancel(() => clearTimeout(timer));
})
  .then((value) => value + 1)
  .then(
    (value) => console.log(`Done: ${value}`),
    (err, scope) => {
      console.warn(err); // CanceledError: canceled
      console.log(`isCanceled: ${scope.isCanceled}`); // true
    }
  );

setTimeout(() => {
  chain.cancel();
}, 100);

使用 AbortController 的相同事情 (实时演示)
import CPromise from "c-promise2";

const controller= new CPromise.AbortController();

new CPromise((resolve, reject, { onCancel }) => {
  const timer = setTimeout(resolve, 1000, 123);
  onCancel(() => clearTimeout(timer));
})
  .then((value) => value + 1)
  .then(
    (value) => console.log(`Done: ${value}`),
    (err, scope) => {
      console.warn(err);
      console.log(`isCanceled: ${scope.isCanceled}`);
    }
  ).listen(controller.signal);

setTimeout(() => {
  controller.abort();
}, 100);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接