取消一个 Promise 链?

9
我将一系列承诺链接起来:
this.getData.then(this.getMoreData).then(this.getEvenMoreData);

在某些时候,用户可能会决定取消请求并请求其他内容。

我该如何取消链的传播?


1
你所说的“用户”是什么意思?这是一段尽可能快速执行的代码,你如何在这里添加用户干预? - tadman
该网站正在获取大量数据。如果用户请求其他数据,则希望停止获取当前数据集并输出它。 - panthro
那么这将如何实现?是否有一个状态对象来表示该请求已被取消,例如 var state = { running: true },然后您可以根据需要测试 if (state.running) { return this.getMoreData() } - tadman
如果您通过ajax获取数据,您可以使用XMLHttpRequest对象上的abort方法取消请求。参见:https://dev59.com/VXRB5IYBdhLWcg3w-8Ho. - Nick Russler
如果你使用bluebird promises,请查看:https://github.com/petkaantonov/bluebird/blob/master/API.md#cancellation 。基本上,取消仅通过抛出一个自定义对象来实现,然后您可以捕获它。 - Safari
显示剩余3条评论
3个回答

8
你需要检查每个链接方法内部的状态(是否应该取消):
var userRequestedCancel = false;

this
   .getData()
   .then(function() {
     if(userRequestedCancel) {
      return Promise.reject('user cancelled');
     }

     return getMoreData();
   })
   .then(function() {
     if(userRequestedCancel) {
      return Promise.reject('user cancelled');
     }

     return getEvenMoreData();
   })

或许有一种稍微更优雅的方式(已编辑为将上下文和参数传递给回调方法)。
var currentReq = false;
var userRequestedCancel = false;
var shouldContinue = function(cb,args) {
    if(userRequestedCancel) {
        return Promise.reject('user cancelled');
    }

    currentReq = cb.apply(this,args);
    return currentReq;
}

var onCancel = function() {
    userRequestedCancel = true;
    currentReq && currentReq.abort();
}

this
   .getData()
   .then(function() {
    return shouldContinue(getMoreData,arguments);
   })
   .then(function() {
     return shouldContinue(getEvenMoreData,arguments);
   })

如果您需要同时取消当前请求,那么可以将当前的ajax请求设置为全局变量,并且无论哪个事件将userRequestedCancel标志设置为true,都会取消ajax请求(请参见上面编辑过的代码)。

1
假设getData()是某个ajax请求,当尚未收到答案时,应该中止该请求。 - Nick Russler
@NickRussler - 概念上并不更加困难。请查看编辑后的代码 - 保留对当前ajax请求的引用,每当您将userRequestedCancel设置为true时,也取消上一个请求。 - Adam Jenkins
1
是的,我提到这个是因为楼主说“网站正在获取大量数据[...]”。 - Nick Russler
谢谢 - 这里的双 && 是什么意思?currentReq && currentReq.abort(); - panthro
@panthro - 这是一个简写的if语句 - 如果左侧的语句(即currentReq)为真,则评估&&后面的内容 - 这与说if(currentReq) { currentReq.abort(); } 是相同的。https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators - Adam Jenkins
显示剩余2条评论

3
为了取消一个承诺链,您需要抛出一个错误。请看下面的代码。
function CancelError() {
    this.message = 'Cancelled';
}


obj
    .then(function() {
        throw new CancelError();
    })
    .catch(function(err) {
        if (err instanceof CancelError) {
            // Promise got cancelled
        }
        throw err; // throw the other mistakes
    });

2
我该如何在链条之外抛出错误?如果用户启动了一个新的链条,我需要取消当前的链条。 - panthro

0

有趣的小挑战!

如果不知道你正在启动哪个任务、请求或进程,以及用户如何中断该进程,那么很难推荐任何解决方案来“打破”.then(...)链,而不进行一些会触发.catch(...)拒绝回调的黑客/技巧。

话虽如此,请看看这个示例是否有所启发。

特别注意makeInterruptablePromise函数及其用法:

var bar = $('.progress-bar');
var h3 = $("h3");
var isEscape;

function log(msg, replace) {
  h3[replace ? 'html' : 'append'](msg + "<br/>");
}

$(document).keydown(e => {
  switch(e.keyCode) {
    case 27: //ESCAPE
      return isEscape = true;
    case 32: //SPACE
      return runDemo();
  }
});

function makeInterruptablePromise(cbStatus) {
  return new Promise((resolve, reject) => {
    function loop() {
      switch(cbStatus()) {
        case 1: return resolve();
        case -1: return reject();
        default: requestAnimationFrame(loop);
      }
    }
    //Don't forget to start the loop!
    loop();
  })
}


function runDemo() {
  log("Wait for it... (ESC to interrupt, SPACE to replay)", true);
  
  isEscape = false;
  
  var timeToComplete = 2000;
  var timeStart = Date.now();
  
  function updateBar() {
    var timeDiff = Date.now() - timeStart;
    var timePercent = timeDiff / timeToComplete;
    TweenMax.set(bar, {scaleX: 1 - timePercent});
    return timePercent > 1;
  }
  
  makeInterruptablePromise(() => {
    if(isEscape) return -1;
    if(updateBar()) return 1;

    return 0;
  })
    .then(() => log("Inside *then* chain."))
    .catch(() => log("Skipped *then* chain!"))
}

runDemo(); //Run first time.
body {
  background-color: #123456;
  color: #fff;
}

.progress-bar {
  display: block;
  width: 200px;
  height: 10px;
  background-color: #88f;
  transform-origin: top left;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.2/TweenMax.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="progress-bar"></div>

<h3></h3>

这实际上归结为:我正在向makeInterruptablePromise传递一个回调函数来“监视”3种可能的状态。

  • 如果是1:它会解决(进入您的' then ')
  • 如果是-1:它会拒绝(跳过到您的' catch ')
  • 否则,它只是通过使用浏览器的requestAnimationFrame(...)方法不断循环。
    (本质上是根据屏幕刷新校准触发的setTimeout(...)

现在,为了影响这些状态随时间变化的方式,我使用ESCAPE作为中断状态(-1),并运行2秒钟的定时器来演示这一点。完成后,定时器返回状态(1)。

不确定它是否符合您的需求,但对于任何试图通过某些外部/异步因素打破Promises的人可能会有用。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接