取消一个vanilla ECMAScript 6 Promise链

dx_*_*_dt 88 javascript promise cancellation es6-promise

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

我在QUnit上编写了一个JavaScript测试框架.框架通过在a中运行每个框架来同步运行测试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);
        });
    });
}
Run Code Online (Sandbox Code Playgroud)

如果测试超时,我的超时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);
});
Run Code Online (Sandbox Code Playgroud)

我在catch声明中设置了一个断点,它正被击中.我现在感到困惑的是,then()声明没有被调用.想法?

更新3

想出最后一件事. fn.call()抛出了一个我没有抓到的错误,因此测试承诺在拒绝之前at.promise.catch()就可以解决它.

Ber*_*rgi 58

有没有一种清除.thenJavaScript Promise实例的方法?

不.至少在ECMAScript 6中没有.Promise(和他们的then处理程序)默认是不可取消的(不幸的是).关于如何以正确的方式进行es-discuss(例如这里)的讨论有一些讨论,但无论采用何种方法,它都不会落在ES6中.

目前的观点是子类化将允许使用您自己的实现创建可取消的承诺(不确定它将如何工作).

直到语言委员会找到最好的方法(ES7希望?),你仍然可以使用userland Promise实现,其中许多功能取消.

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

  • Domenic [删除了TC39提案......](https://github.com/tc39/proposal-cancelable-promises)... cc @BenjaminGruenbaum (6认同)
  • “一些讨论” - 我可以链接到 esdiscuss 或 GitHub 上的 30 个线程:)(更不用说你自己在 bluebird 3.0 中的取消帮助了) (3认同)

Mic*_*aev 48

虽然在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
Run Code Online (Sandbox Code Playgroud)

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

  • 这个取消的定义只是拒绝承诺。这取决于“取消”的定义。 (4认同)
  • 这在一定程度上是正确的,但如果你有很长的承诺链,这种方法就行不通。 (3认同)
  • 这种方法的问题是,如果你有一个永远不会解决或拒绝的 Promise,它就永远不会被取消。 (2认同)

Soh*_*raf 9

可以在 的帮助下取消 Promise AbortController

那么是否有清除方法: 是的,您可以拒绝带有AbortController对象的承诺,然后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);

Run Code Online (Sandbox Code Playgroud)

html


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

Run Code Online (Sandbox Code Playgroud)

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

现场示例

编辑优雅湖5jnh3

  • 另外值得注意的是:fetch api 调用可以选择接受信号,从而允许中止请求。请参阅 https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal (2认同)

Pho*_*Hun 8

我真的很惊讶,没有人提到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 });
Run Code Online (Sandbox Code Playgroud)

  • 是的,它的工作原理实际上有点微妙,但它确实有效。`Promise` 构造函数同步运行,因此已经运行,并且无法取消。它调用“setTimeout”来安排一个将解决承诺的事件,**但该事件不是承诺链的一部分**。调用“cancel”会导致可取消的 Promise 被拒绝,因此当它解析时链接到运行的任何内容都将被丢弃而不会运行。`setTimeout` 事件处理程序最终仍然运行,但无法解决已经拒绝的承诺;如果你想取消超时本身,你需要一个不同的机制。 (6认同)
  • 我不相信这行得通。如果您将承诺更改为日志,运行`cancel()`仍会导致调用日志。``const actualPromise = new Promise((resolve,reject)=&gt; {setTimeout(()=&gt; {console.log('actual named'); resolve()},10000)});; ``` (2认同)
  • 问题是如何取消承诺(=&gt;停止链接“然后执行”),而不是如何取消“ setTimeout”(=“ clearTimeout”)或同步代码,除非您在每行之后放置一个if(“ if(取消)返回”),否则无法实现。(不要这样做) (2认同)

Sla*_*a M 7

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;
};
Run Code Online (Sandbox Code Playgroud)

用法:

const cancelablePromise = makeCancelable(myPromise);
// ...
cancelablePromise.cancel();
Run Code Online (Sandbox Code Playgroud)


nik*_*san 6

阻止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;
  }
}
Run Code Online (Sandbox Code Playgroud)

用法:

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

p.catch(console.log);

setTimeout(() => {
  p.cancel(new Error('Messed up!'));
}, 1000);
Run Code Online (Sandbox Code Playgroud)