Promise - 是否可以强制取消Promise

IT技术 javascript promise cancellation
2021-02-01 22:19:45

我使用 ES6 Promises 来管理我所有的网络数据检索,在某些情况下我需要强制取消它们。

基本上这个场景是这样的,我在 UI 上有一个预先输入的搜索,其中请求被委托给后端必须根据部分输入执行搜索。虽然这个网络请求 (#1) 可能需要一点时间,但用户继续输入最终会触发另一个后端调用 (#2)

这里#2 自然优先于#1,所以我想取消Promise 包装请求#1。我已经在数据层中缓存了所有 Promise,因此理论上我可以在尝试为 #2 提交 Promise 时检索它。

但是,一旦我从缓存中检索它,我该如何取消 Promise #1?

有人可以建议一种方法吗?

6个回答

不,我们还不能那样做。

ES6Promise不支持取消还没有它正在路上,它的设计是很多人非常努力的工作。声音消除语义很难做到正确,这是正在进行中的工作。关于“获取”存储库、esdiscuss 和其他几个关于 GH 的存储库存在有趣的争论,但如果我是你,我会耐心等待。

但是,但是,但是……取消真的很重要!

事实是,取消确实是客户端编程中的一个重要场景。您描述的诸如中止 Web 请求之类的情况很重要,而且无处不在。

所以……语言把我搞砸了!

是的,对不起。Promise必须得到在第一前进一步东西都规定-所以他们就去了,没有像一些有用的东西.finally,并.cancel-这是在它的途中,虽然,通过DOM规范。取消不是事后的想法,它只是时间限制和 API 设计的一种更迭代的方法。

那我能做什么?

您有几种选择:

  • 使用像bluebird这样的第三方库,它的移动速度比规范快得多,因此可以取消以及其他一些好东西——这就是像 WhatsApp 这样的大公司所做的。
  • 传递一个取消令牌

使用第三方库是很明显的。至于令牌,您可以让您的方法接受一个函数,然后调用它,如下所示:

function getWithCancel(url, token) { // the token is for cancellation
   var xhr = new XMLHttpRequest;
   xhr.open("GET", url);
   return new Promise(function(resolve, reject) {
      xhr.onload = function() { resolve(xhr.responseText); });
      token.cancel = function() {  // SPECIFY CANCELLATION
          xhr.abort(); // abort request
          reject(new Error("Cancelled")); // reject the promise
      };
      xhr.onerror = reject;
   });
};

这会让你做:

var token = {};
var promise = getWithCancel("/someUrl", token);

// later we want to abort the promise:
token.cancel();

您的实际用例 - last

使用令牌方法这并不太难:

function last(fn) {
    var lastToken = { cancel: function(){} }; // start with no op
    return function() {
        lastToken.cancel();
        var args = Array.prototype.slice.call(arguments);
        args.push(lastToken);
        return fn.apply(this, args);
    };
}

这会让你做:

var synced = last(getWithCancel);
synced("/url1?q=a"); // this will get canceled 
synced("/url1?q=ab"); // this will get canceled too
synced("/url1?q=abc");  // this will get canceled too
synced("/url1?q=abcd").then(function() {
    // only this will run
});

不,像 Bacon 和 Rx 这样的库在这里不会“发光”,因为它们是可观察的库,它们只是具有与用户级Promise库相同的优势,不受规范约束。我想我们会等待并在 ES2016 中看到 observables 成为原生的时候。不过,它们非常适合提前输入。

@harm 该提案在第 1 阶段已失效。
2021-03-14 22:19:45
本杰明,非常喜欢阅读您的回答。深思熟虑,结构化,清晰,并具有良好的实际示例和替代方案。真的很有帮助。谢谢你。
2021-03-17 22:19:45
我们在哪里可以阅读这个基于令牌的取消?提案在哪里?
2021-03-28 22:19:45
@FranciscoPresencia 取消代币正在作为第一阶段的提案。
2021-04-04 22:19:45
我喜欢 Ron 的作品,但我认为我们应该稍等片刻,然后再为人们尚未使用的图书馆提出建议:] 感谢您提供链接,但我会检查一下!
2021-04-07 22:19:45

可取消Promise的标准提案失败了。

Promise不是实现它的异步操作的控制面;混淆所有者和消费者。相反,创建可以通过一些传入令牌取消的异步函数

另一个Promise是一个很好的令牌,使取消易于实现Promise.race

例如:使用Promise.race取消先前链的影响:

let cancel = () => {};

input.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancel();
  let p = new Promise(resolve => cancel = resolve);
  Promise.race([p, getSearchResults(term)]).then(results => {
    if (results) {
      console.log(`results for "${term}"`,results);
    }
  });
}

function getSearchResults(term) {
  return new Promise(resolve => {
    let timeout = 100 + Math.floor(Math.random() * 1900);
    setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
  });
}
Search: <input id="input">

在这里,我们通过注入一个undefined结果并对其进行测试来“取消”之前的搜索,但我们可以很容易地想象用"CancelledError"代替来拒绝

当然,这实际上并没有取消网络搜索,但这是fetch. 如果fetch将取消Promise作为参数,那么它可以取消网络活动。

在 es-discuss 上提出了这个“取消Promise模式”,正是为了建议这样fetch做。

@jib 为什么拒绝我的修改?我只是澄清一下。
2021-03-31 22:19:45

使用 AbortController

可以使用 abort 控制器拒绝Promise或根据您的要求解决:

let controller = new AbortController();

let task = new Promise((resolve, reject) => {
  // some logic ...
  controller.signal.addEventListener('abort', () => reject('oops'));
});

controller.abort(); // task is now in rejected state

此外,最好在 abort 时删除事件侦听器以防止内存泄漏

取消 fetch 的工作原理相同:

let controller = new AbortController();
fetch(url, {
  signal: controller.signal
});

或者只是通过控制器:

let controller = new AbortController();
fetch(url, controller);

并调用 abort 方法来取消您传递此控制器的一次或无限次提取 controller.abort();

我检查了 Mozilla JS 参考并发现了这个:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race

让我们来看看:

var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, "one"); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, "two"); 
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value); // "two"
  // Both resolve, but p2 is faster
});

我们在这里将 p1 和 p2Promise.race(...)作为参数放入,这实际上是在创建新的解析Promise,这正是您所需要的。

试过了。不完全在那里。这解决了最快的Promise......我需要始终解决最新提交的问题,即无条件取消任何旧的Promise..
2021-03-15 22:19:45
如果您遇到问题,可以在此处粘贴代码,以便我为您提供帮助:)
2021-03-30 22:19:45
这样就不再处理所有其他Promise,您实际上无法取消Promise。
2021-03-31 22:19:45
我试过了,第二个Promise(本例中的一个)不要让进程退出:(
2021-04-02 22:19:45
NICE - 这可能正是我需要的。我会试试看。
2021-04-09 22:19:45

对于 Node.js 和 Electron,我强烈推荐使用Promise Extensions for JavaScript (Prex)它的作者Ron Buckton是 TypeScript 的关键工程师之一,也是当前 TC39 的ECMAScript Cancellation提案的幕后推手该库有很好的文档记录,并且 Prex 有可能符合标准。

就个人而言,来自 C# 背景,我非常喜欢 Prex以托管线程框架中现有的取消为模型的事实,即基于CancellationTokenSource/ CancellationToken.NET API所采用的方法根据我的经验,这些对于在托管应用程序中实现强大的取消逻辑非常方便。

我还通过使用Browserify捆绑 Prex 来验证它可以在浏览器中工作

下面是一个取消延迟的例子(GistRunKit,使用Prex作为它的CancellationTokenand Deferred):

// by @noseratio
// https://gist.github.com/noseratio/141a2df292b108ec4c147db4530379d2
// https://runkit.com/noseratio/cancellablepromise

const prex = require('prex');

/**
 * A cancellable promise.
 * @extends Promise
 */
class CancellablePromise extends Promise {
  static get [Symbol.species]() { 
    // tinyurl.com/promise-constructor
    return Promise; 
  }

  constructor(executor, token) {
    const withCancellation = async () => {
      // create a new linked token source 
      const linkedSource = new prex.CancellationTokenSource(token? [token]: []);
      try {
        const linkedToken = linkedSource.token;
        const deferred = new prex.Deferred();
  
        linkedToken.register(() => deferred.reject(new prex.CancelError()));
  
        executor({ 
          resolve: value => deferred.resolve(value),
          reject: error => deferred.reject(error),
          token: linkedToken
        });

        await deferred.promise;
      } 
      finally {
        // this will also free all linkedToken registrations,
        // so the executor doesn't have to worry about it
        linkedSource.close();
      }
    };

    super((resolve, reject) => withCancellation().then(resolve, reject));
  }
}

/**
 * A cancellable delay.
 * @extends Promise
 */
class Delay extends CancellablePromise {
  static get [Symbol.species]() { return Promise; }

  constructor(delayMs, token) {
    super(r => {
      const id = setTimeout(r.resolve, delayMs);
      r.token.register(() => clearTimeout(id));
    }, token);
  }
}

// main
async function main() {
  const tokenSource = new prex.CancellationTokenSource();
  const token = tokenSource.token;
  setTimeout(() => tokenSource.cancel(), 2000); // cancel after 2000ms

  let delay = 1000;
  console.log(`delaying by ${delay}ms`); 
  await new Delay(delay, token);
  console.log("successfully delayed."); // we should reach here

  delay = 2000;
  console.log(`delaying by ${delay}ms`); 
  await new Delay(delay, token);
  console.log("successfully delayed."); // we should not reach here
}

main().catch(error => console.error(`Error caught, ${error}`));

请注意,取消是一场竞赛。即,一个Promise可能已成功解决,但当您观察它时(使用awaitthen),取消也可能已被触发。这取决于你如何处理这场比赛,但token.throwIfCancellationRequested()像我上面做的那样,打电话给额外的时间并没有什么坏处