【问题标题】: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,如何取消它?

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

【问题讨论】:

标签: javascript promise cancellation


【解决方案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 来验证它可以在浏览器中工作。

以下是取消延迟的示例(GistRunKit,使用 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 ${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() 并没有什么坏处。

【讨论】:

    【解决方案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();

            【讨论】:

            • 这似乎是一种很好的现代方法。
            猜你喜欢
            • 1970-01-01
            • 2019-07-15
            • 2017-10-09
            • 2018-12-10
            • 1970-01-01
            • 2014-03-13
            • 2015-01-23
            • 2015-02-13
            相关资源
            最近更新 更多