承诺-是否可以强制取消承诺


91

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

基本上,这种情况是这样的:我在UI上进行了预先输入搜索,在该UI中,将请求委派给后端必须基于部分输入执行搜索。尽管此网络请求(#1)可能需要一点时间,但用户继续输入,最终会触发另一个后端调用(#2)

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

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

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


2
这是使用等同于反跳功能的选项,而不是经常触发并成为过时的请求的选择吗?说300毫秒的延迟就可以解决问题。例如,Lodash具有以下实现之一-lodash.com/docs#debounce
shershen,2015年

这是培根和Rx等有用的东西。
Elclanrs

@shershen是的-我们有这个,但与UI问题无关。.服务器查询可能需要一些时间,所以我希望能够取消Promises ...
Moonwalker


在Rxjs中尝试Observables
FieryCod's

Answers:


163

不,我们还不能这样做。

ES6承诺不支持取消还没有。它正在进行中,其设计是很多人真正努力的工作。声音消除的语义很难正确理解,这正在进行中。关于“访存”回购,esdiscuss以及关于GH的其他几个回购,有很多有趣的辩论,但是如果我是您,请耐心等待。

但是,但是,..取消确实很重要!

事实是,取消实际上是客户端编程中的重要场景。您描述的情况(例如中止Web请求)非常重要,并且无处不在。

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

是的,对此感到抱歉。承诺必须得到在第一前进一步东西都规定-所以他们就去了,没有像一些有用的东西.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这样的库在这里不会“发光”,因为它们是可观察的库,它们具有与用户级别承诺的库相同的优势,因为它们不受规范的约束。我猜我们将等到可观察性技术在ES2016中出现。他们不错的提前输入。


25
本杰明,非常喜欢阅读您的答案。经过深思熟虑,结构清晰,清晰明了,并提供了良好的实践示例和替代方案。真的很有帮助。谢谢。
行者2015年

@FranciscoPresencia取消令牌正在作为第1阶段的提案。
本杰明·格林鲍姆

我们在哪里可以读到基于令牌的取消?提案在哪
伤害

@harm的建议是死在阶段1
本杰明Gruenbaum

1
我喜欢罗恩(Ron)的工作,但我认为我们应该稍等片刻,再为尚未使用的图书馆提出建议:]尽管我会检查链接,但感谢您的链接!
本杰明·格伦鲍姆

24

可取消承诺的标准提案已失败。

一个承诺不是实现它的异步动作的控制面;使所有者与消费者混淆。而是创建可以通过某些传入令牌取消的异步函数

另一个promise可以作为一个很好的令牌,使cancel易于实现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以取消承诺作为参数,则可以取消网络活动。

我已经在es-discuss上提出了这种“取消承诺模式”,正好建议这样fetch做。


@jib为什么拒绝我的修改?我只是澄清一下。
allenyllee19年

8

我已经查看了Mozilla JS参考,发现了这一点:

https://developer.mozilla.org/zh-CN/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(...)作为参数,这实际上是在创建新的解决承诺,这是您所需要的。


尼斯-这也许正是我所需要的。我会试试看。
Moonwalker

如果您有问题,可以在此处粘贴代码,以便我为您提供帮助:)
nikola-miljkovic,2015年

5
试过了 不完全在那里。这解决了最快的承诺......我需要时刻解决提交即无条件最新取消任何旧承诺..
夜光杯

1
这样,不再处理所有其他承诺,则您实际上无法取消承诺。
nikola-miljkovic

我尝试过,第二个承诺(在此之前的一个)不让进程退出:(
morteza ataiy,

3

对于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}`));

请注意,取消是一场比赛。即,一个承诺可能已经成功解决,但是当您观察到它(使用awaitthen)时,取消触发也可能已经触发。这取决于您如何处理这场比赛,但是token.throwIfCancellationRequested()像我上面所说的那样,加时赛并没有什么坏处。


1

我最近遇到了类似的问题。

我有一个基于Promise的客户端(不是网络客户端),并且我想始终将最新请求的数据提供给用户,以保持UI流畅。

与消除思想挣扎后,Promise.race(...)Promise.all(..)我刚开始记住我最后的请求ID和承诺应验了,那时我才呈现我的数据时,它相匹配的最后一个请求的ID。

希望它能帮助某人。


Slomski的问题不在于在UI上显示什么。关于取消承诺
Cyber​​Abhay


0

您可以在完成之前使诺言拒绝:

// Our function to cancel promises receives a promise and return the same one and a cancel function
const cancellablePromise = (promiseToCancel) => {
  let cancel
  const promise = new Promise((resolve, reject) => {
    cancel = reject
    promiseToCancel
      .then(resolve)
      .catch(reject)
  })
  return {promise, cancel}
}

// A simple promise to exeute a function with a delay
const waitAndExecute = (time, functionToExecute) => new Promise((resolve, reject) => {
  timeInMs = time * 1000
  setTimeout(()=>{
    console.log(`Waited ${time} secs`)
    resolve(functionToExecute())
  }, timeInMs)
})

// The promise that we will cancel
const fetchURL = () => fetch('https://pokeapi.co/api/v2/pokemon/ditto/')

// Create a function that resolve in 1 seconds. (We will cancel it in 0.5 secs)
const {promise, cancel} = cancellablePromise(waitAndExecute(1, fetchURL))

promise
  .then((res) => {
    console.log('then', res) // This will executed in 1 second
  })
  .catch(() => {
    console.log('catch') // We will force the promise reject in 0.5 seconds
  })

waitAndExecute(0.5, cancel) // Cancel previous promise in 0.5 seconds, so it will be rejected before finishing. Commenting this line will make the promise resolve

不幸的是,提取呼叫已经完成,因此您将在“网络”选项卡中看到呼叫正在解析。您的代码只会忽略它。


0

使用外部包提供的Promise子类,可以按以下步骤完成:现场演示

import CPromise from "c-promise2";

function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
    return new CPromise((resolve, reject, {signal}) => {
        fetch(url, {...fetchOptions, signal}).then(resolve, reject)
    }, timeout)
}

const chain= fetchWithTimeout('http://localhost/')
    .then(response => response.json())
    .then(console.log, console.warn);

//chain.cancel(); call this to abort the promise and releated request

-1

因为@jib拒绝我的修改,所以我在这里发布我的答案。它只是@jib的anwser的修改,带有一些注释并使用了更易理解的变量名。

下面,我仅显示两种不同方法的示例:一种是resolve(),另一种是reject()

let cancelCallback = () => {};

input.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancelCallback(); //cancel previous promise by calling cancelCallback()

  let setCancelCallbackPromise = () => {
    return new Promise((resolve, reject) => {
      // set cancelCallback when running this promise
      cancelCallback = () => {
        // pass cancel messages by resolve()
        return resolve('Canceled');
      };
    })
  }

  Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
    // check if the calling of resolve() is from cancelCallback() or getSearchResults()
    if (results == 'Canceled') {
      console.log("error(by resolve): ", results);
    } else {
      console.log(`results for "${term}"`, results);
    }
  });
}


input2.oninput = function(ev) {
  let term = ev.target.value;
  console.log(`searching for "${term}"`);
  cancelCallback(); //cancel previous promise by calling cancelCallback()

  let setCancelCallbackPromise = () => {
    return new Promise((resolve, reject) => {
      // set cancelCallback when running this promise
      cancelCallback = () => {
        // pass cancel messages by reject()
        return reject('Canceled');
      };
    })
  }

  Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
    // check if the calling of resolve() is from cancelCallback() or getSearchResults()
    if (results !== 'Canceled') {
      console.log(`results for "${term}"`, results);
    }
  }).catch(error => {
    console.log("error(by reject): ", error);
  })
}

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

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.