承诺 - 是否有可能强制取消承诺

时间:2015-05-14 09:03:45

标签: javascript promise cancellation

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

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

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

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

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

10 个答案:

答案 0 :(得分:105)

否。我们还不能那样做。

ES6承诺不支持取消尚未。它正在进行中,其设计是许多人非常努力的工作。 声音取消语义很难理解,这是正在进行的工作。关于“fetch”回购,有关es还有关于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这样的库在这里没有“闪耀”,因为它们是可观察的库,它们具有与用户级别承诺库相同的优势,而不受规范约束。我想我们会等到ES2016看到可观测量出现的时候。他们 非常适合打字。

答案 1 :(得分:13)

可撤销承诺的标准提案失败。

承诺不是实现它的异步操作的控制界面;使所有者与消费者混淆。相反,创建可通过某些传入令牌取消的异步 函数

另一个承诺是一个很好的令牌,使用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">
&#13;
&#13;
&#13;

我们在此取消&#34;取消&#34;之前的搜索通过注入undefined结果并对其进行测试,但我们可以轻易地想象拒绝使用"CancelledError"

当然,这并没有取消网络搜索,但这是fetch的限制。如果fetch将取消承诺作为参数,那么它可以取消网络活动。

proposed这个&#34;取消承诺模式&#34;在es-discuss上,正好建议fetch这样做。

答案 2 :(得分:6)

我已经查看了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(...),这实际上是创建了新的解析承诺,这就是你需要的。

答案 3 :(得分:5)

使用 AbortController

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

Product::orderByRaw("date_format(created_at ,'%Y-%m-%d') = ? desc, view desc", [$today])
         ->get();

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

同样适用于取消提取:

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
});

并调用 abort 方法来取消您传递此控制器的一次或无限次提取 let controller = new AbortController(); fetch(url, controller);

答案 4 :(得分:2)

我建议使用Promise Extensions for JavaScript (Prex)。它的作者Ron Buckton是TypeScript的关键工程师之一,也是当前TC39的ECMAScript Cancellation提案的幕后推手。该库有充分的文档记录,Prex可能会符合标准。

就个人而言,具有浓厚的C#背景,我非常喜欢Prex是基于现有Cancellation in Managed Threads框架(即基于CancellationTokenSource / {{ 1}} .NET API。以我的经验,在托管应用程序中实施健壮的取消逻辑非常方便。

以下是在节点上使用prex.CancellationTokenSource进行取消的延迟的示例:

CancellationToken

请注意,取消是一场比赛。也就是说,一个承诺可能已经成功解决,但是当您观察到承诺时(使用const prex = require('prex'); async function delayWithCancellation(timeoutMs, token) { // this can easily be done without async/await, // but I believe this linear structure is more readable let reg = null; try { await new Promise((resolve, reject) => { const id = setTimeout(resolve, timeoutMs); reg = token.register(() => { clearTimeout(id); reject(new prex.CancelError("delay cancelled.")); }); }); } finally { reg && reg.unregister(); } } async function main() { const tokenSource = new prex.CancellationTokenSource(); setTimeout(() => tokenSource.cancel(), 1500); // cancel after 1500ms // without cancellation await delayWithCancellation(1000, prex.CancellationToken.none); console.log("successfully delayed once."); // with cancellation const token = tokenSource.token; await delayWithCancellation(1500, token); token.throwIfCancellationRequested(); console.log("successfully delayed twice."); // we should not be here } main().catch(e => console.log(e)); await),取消触发也可能已经触发。这取决于您如何处理这场比赛,但是像上面我所说的那样,多打then也不会有什么坏处。

已更新,我通过取消支持扩展了标准的本机Promise class,类似于implemented in Bluebird(即,带有可选的token.throwIfCancellationRequested()回调),但使用oncancelhere提供了prex.CancellationTokenSource的代码。

答案 5 :(得分:0)

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

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

在为取消想法苦苦挣扎之后,Promise.race(...)Promise.all(..)开始记住我的上一个请求ID,当诺言实现时,我只在与上一个请求的ID匹配时才呈现我的数据。 / p>

希望它可以帮助某人。

答案 6 :(得分:0)

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

$ npm install promise-abortable

答案 7 :(得分:0)

因为@jib拒绝我的修改,所以我在这里发布我的答案。它只是@jib's 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">

答案 8 :(得分: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

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

答案 9 :(得分:0)

使用外部软件包提供的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