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

Posted

技术标签:

【中文标题】承诺 - 是不是可以强制取消承诺【英文标题】:Promise - is it possible to force cancel a promise承诺 - 是否可以强制取消承诺 【发布时间】:2015-07-25 19:57:28 【问题描述】:

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

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

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

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

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

【问题讨论】:

是否可以选择使用某种等效的去抖动功能来不经常触发并成为过时的请求?说 300 毫秒的延迟就可以了。例如 Lodash 有其中一种实现 - lodash.com/docs#debounce 这时候培根和 Rx 之类的东西就派上用场了。 @shershen 是的 - 我们有这个,但这与 UI 问题无关。服务器查询可能需要一些时间,所以我希望能够取消 Promise ... 相关:Status of cancellable promises、How to cancel an ES6 promise chain、How to cancel timeout inside of javascript Promise? 从 Rxjs 尝试 Observables 【参考方案1】:

我查看了 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,p2 把 Promise.race(...) 作为参数,这实际上是在创建新的 resolve promise,这是你需要的。

【讨论】:

NICE - 这也许正是我所需要的。我会试一试的。 如果你有问题,你可以在这里粘贴代码,我可以帮助你:) 试过了。不完全在那里。这解决了最快的 Promise...我需要始终解决最新提交的问题,即无条件取消任何较旧的 Promise.. 这种方式不再处理所有其他承诺,您实际上无法取消承诺。 我试过了,第二个承诺(这个前一个)不要让进程退出:(【参考方案2】:

没有。我们还不能这样做。

ES6 Promise 还不支持取消。它正在路上,它的设计是很多人努力工作的。 声音 取消语义很难正确处理,这项工作正在进行中。关于“fetch” repo、esdiscuss 和 GH 上的其他几个 repo 有一些有趣的辩论,但如果我是你,我会耐心等待。

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

事实上,取消是真的客户端编程中的一个重要场景。您描述的中止 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 这样的库在这里不会“发光”,因为它们是可观察的库,它们只是具有与用户级承诺库相同的优势,即不受规范约束。我想我们会等待在 ES2016 中看到 observables 原生化。不过,它们 很适合预先输入。

【讨论】:

Benjamin,非常喜欢阅读您的回答。深思熟虑,结构清晰,口齿清晰,并带有良好的实际示例和替代方案。真的很有帮助。谢谢。 @FranciscoPresencia 取消令牌正在作为第 1 阶段提案进行中。 我们在哪里可以了解这个基于令牌的取消?提案在哪里? @harm 提案在第 1 阶段已失效。 我喜欢 Ron 的工作,但我认为我们应该稍等片刻,然后再为人们尚未使用的图书馆提出建议:] 感谢您提供链接,但我会检查一下!【参考方案3】:

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

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 将取消承诺作为参数,那么它可以取消网络活动。

我在 es-discuss 上 proposed 这个“取消承诺模式”,正是为了建议 fetch 这样做。

【讨论】:

@jib 为什么拒绝我的修改?我只是澄清一下。【参考方案4】:

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

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

在与取消想法苦苦挣扎之后,Promise.race(...)Promise.all(..) 我才开始记住我的上一个请求 ID,并且当承诺兑现时,我仅在与上一个请求的 ID 匹配时才呈现我的数据。

希望对某人有所帮助。

【讨论】:

Slomski 问题不在于在 UI 上显示什么。关于取消承诺【参考方案5】:

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

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

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

以下是取消延迟的示例(Gist 和 RunKit,使用 Prex 作为其 CancellationTokenDeferred):

// 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 $delayms`); 
  await new Delay(delay, token);
  console.log("successfully delayed."); // we should reach here

  delay = 2000;
  console.log(`delaying by $delayms`); 
  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() 并没有什么坏处。

【讨论】:

【参考方案6】:

见https://www.npmjs.com/package/promise-abortable

$ npm install promise-abortable

【讨论】:

【参考方案7】:

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

下面我只展示两种不同方法的示例:一种是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">

【讨论】:

【参考方案8】:

你可以在完成之前让 Promise 被拒绝:

// 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

不幸的是,fetch 调用已经完成,因此您将在 Network 选项卡中看到调用正在解析。您的代码将忽略它。

【讨论】:

【参考方案9】:

使用外部包提供的Promise子类,可以这样做:Live demo

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

【讨论】:

【参考方案10】:

使用 AbortController

可以使用中止控制器来拒绝承诺或根据您的要求解决:

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

另外最好在中止时移除事件监听器以防止内存泄漏

同样适用于取消提取:

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

或者只是传递控制器:

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

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

【讨论】:

这似乎是一种很好的现代方法。

以上是关于承诺 - 是不是可以强制取消承诺的主要内容,如果未能解决你的问题,请参考以下文章

如何在 node.js 中的特定持续时间后强制解决承诺? [复制]

在 ReactJS 中卸载组件时取消承诺

javascript 带有取消的多个承诺链

javascript 取消承诺链

承诺等待后取消 lodash debounce

猫鼬承诺文档说查询不是承诺?