中断承诺链并根据中断(拒绝)的链中的步骤调用函数

更新:

为了帮助这篇文章的未来观众,我创建了 这个李子的答案的演示。

问题:

我的目标看起来很简单。

  step(1)
.then(function() {
return step(2);
}, function() {
stepError(1);
return $q.reject();
})
.then(function() {


}, function() {
stepError(2);
});


function step(n) {
var deferred = $q.defer();
//fail on step 1
(n === 1) ? deferred.reject() : deferred.resolve();
return deferred.promise;
}
function stepError(n) {
console.log(n);
}

这里的问题是,如果我在步骤1中失败,那么就会触发 stepError(1)stepError(2)。如果我没有 return $q.reject,那么 stepError(2)将不会被解雇,但 step(2)将,这是我理解的。我已经完成了一切,除了我想做的。

如何编写承诺以便在拒绝时调用函数,而不必调用错误链中的所有函数?或者还有别的办法吗?

这里有一个现场演示 ,所以你已经有了一些工作。

更新:

类型已经解决了。在这里,我在链的末尾捕获错误并将数据传递给 reject(data),这样我就知道在错误函数中要处理什么问题。这实际上不符合我的要求,因为我不想依赖于数据。这样做可能不太好,但在我的例子中,将错误回调传递给函数比依赖返回的数据来确定要做什么更简单。

现场演示(点击)。

step(1)
.then(function() {
return step(2);
})
.then(function() {
return step(3);
})
.then(false,
function(x) {
stepError(x);
}
);
function step(n) {
console.log('Step '+n);
var deferred = $q.defer();
(n === 1) ? deferred.reject(n) : deferred.resolve(n);
return deferred.promise;
}
function stepError(n) {
console.log('Error '+n);
}
189083 次浏览

如果我理解正确,您只希望显示失败步骤的错误,对吗?

这应该很简单,就像把第一个承诺的失败案例改成这样:

step(1).then(function (response) {
step(2);
}, function (response) {
stepError(1);
return response;
}).then( ... )

通过在第一步的失败案例中返回 $q.reject(),您将拒绝该承诺,这将导致在第2个 then(...)中调用 errorCallback。

您的代码之所以不能按预期工作,是因为它实际上所做的事情与您认为它所做的事情不同。

假设你有如下的东西:

stepOne()
.then(stepTwo, handleErrorOne)
.then(stepThree, handleErrorTwo)
.then(null, handleErrorThree);

为了更好地理解发生了什么,让我们假设这是带有 try/catch块的同步代码:

try {
try {
try {
var a = stepOne();
} catch(e1) {
a = handleErrorOne(e1);
}
var b = stepTwo(a);
} catch(e2) {
b = handleErrorTwo(e2);
}
var c = stepThree(b);
} catch(e3) {
c = handleErrorThree(e3);
}

onRejected处理程序(then的第二个参数)本质上是一种错误纠正机制(类似于 catch块)。如果在 handleErrorOne中抛出错误,它将被下一个 catch 块(catch(e2))捕获,依此类推。

这显然不是你的本意。

假设我们希望整个解决方案链无论出现什么问题都失败:

stepOne()
.then(function(a) {
return stepTwo(a).then(null, handleErrorTwo);
}, handleErrorOne)
.then(function(b) {
return stepThree(b).then(null, handleErrorThree);
});

注意: 我们可以保留 handleErrorOne,因为它只有在 stepOne拒绝的情况下才会被调用(它是链中的第一个函数,所以我们知道如果链在这一点上被拒绝,那只能是因为该函数的承诺)。

重要的变化是,其他函数的错误处理程序不是主承诺链的一部分。相反,每个步骤都有自己的“子链”,只有在步骤被拒绝(但主链无法直接到达)时才调用 onRejected

这样做的原因是 onFulfilledonRejected都是 then方法的可选参数。如果一个承诺得到了履行(例如解决) ,而链中的下一个 then没有 onFulfilled处理程序,那么链将继续下去,直到有一个具有这样的处理程序。

这意味着以下两行是等价的:

stepOne().then(stepTwo, handleErrorOne)
stepOne().then(null, handleErrorOne).then(stepTwo)

但是下面这行是 没有,相当于上面两行:

stepOne().then(stepTwo).then(null, handleErrorOne)

Angular 的承诺库 $q是基于 kriskowal 的 Q库(它有一个更丰富的 API,但包含了在 $q中可以找到的所有内容)。Q 在 GitHub 上的 API 文件可能会很有用。Q 实现了 承诺/A + 规格,其中详细介绍了 then和承诺解析行为的确切工作方式。

编辑:

还要记住,如果您想要打破错误处理程序中的链条,它需要返回一个被拒绝的承诺或抛出一个 Error (它将被捕获并自动包装在一个被拒绝的承诺中)。如果不返回承诺,则 then将返回值包装在一个解析承诺中。

这意味着,如果不返回任何内容,那么实际上就是返回一个已解析的值 undefined的承诺。

你所需要的是一个重复的 .then()链与一个特殊的情况下开始和一个特殊的情况下结束。

诀窍是获得失败案例的步数,以波及到最终的错误处理程序。

  • 开始: 无条件调用 step(1)
  • 重复模式: 使用以下回调将 .then()链接起来:
    • 成功: 调用步骤(n + 1)
    • False: 抛出前一个延迟被拒绝的值,或者重新抛出错误。
  • Finish: 链接一个没有成功处理程序和最终错误处理程序的 .then()

你可以手写整个过程,但是用命名的泛化函数来演示这个模式会更容易:

function nextStep(n) {
return step(n + 1);
}


function step(n) {
console.log('step ' + n);
var deferred = $q.defer();
(n === 3) ? deferred.reject(n) : deferred.resolve(n);
return deferred.promise;
}


function stepError(n) {
throw(n);
}


function finalError(n) {
console.log('finalError ' + n);
}
step(1)
.then(nextStep, stepError)
.then(nextStep, stepError)
.then(nextStep, stepError)
.then(nextStep, stepError)
.then(nextStep, stepError)
.then(null, finalError);});

小样

请注意,在 step()中,如何使用 n拒绝或解析延迟,从而使该值可用于链中下一个 .then()中的回调。一旦调用了 stepError,错误将被重复抛出,直到由 finalError处理。

var s = 1;
start()
.then(function(){
return step(s++);
})
.then(function() {
return step(s++);
})
.then(function() {
return step(s++);
})
.then(0, function(e){
console.log(s-1);
});

Http://jsbin.com/epazisip/20/edit

或者自动执行任意数量的步骤:

var promise = start();
var s = 1;
var l = 3;
while(l--) {
promise = promise.then(function() {
return step(s++);
});
}
promise.then(0, function(e){
console.log(s-1);
});

Http://jsbin.com/epazisip/21/edit

将错误处理程序作为单独的链元素直接附加到步骤的执行:

        // Handle errors for step(1)
step(1).then(null, function() { stepError(1); return $q.reject(); })
.then(function() {
// Attach error handler for step(2),
// but only if step(2) is actually executed
return step(2).then(null, function() { stepError(2); return $q.reject(); });
})
.then(function() {
// Attach error handler for step(3),
// but only if step(3) is actually executed
return step(3).then(null, function() { stepError(3); return $q.reject(); });
});

或使用 catch():

       // Handle errors for step(1)
step(1).catch(function() { stepError(1); return $q.reject(); })
.then(function() {
// Attach error handler for step(2),
// but only if step(2) is actually executed
return step(2).catch(function() { stepError(2); return $q.reject(); });
})
.then(function() {
// Attach error handler for step(3),
// but only if step(3) is actually executed
return step(3).catch(function() { stepError(3); return $q.reject(); });
});

注意: 这基本上与 李子在他的回答中暗示相同,但是使用了 OP 的命名。

当拒绝时,您应该传递一个拒绝错误,然后将步骤错误处理程序包装在一个函数中,该函数检查拒绝是否应该处理或“重新抛出”,直到链的末尾:

// function mocking steps
function step(i) {
i++;
console.log('step', i);
return q.resolve(i);
}


// function mocking a failing step
function failingStep(i) {
i++;
console.log('step '+ i + ' (will fail)');
var e = new Error('Failed on step ' + i);
e.step = i;
return q.reject(e);
}


// error handler
function handleError(e){
if (error.breakChain) {
// handleError has already been called on this error
// (see code bellow)
log('errorHandler: skip handling');
return q.reject(error);
}
// firs time this error is past to the handler
console.error('errorHandler: caught error ' + error.message);
// process the error
// ...
//
error.breakChain = true;
return q.reject(error);
}


// run the steps, will fail on step 4
// and not run step 5 and 6
// note that handleError of step 5 will be called
// but since we use that error.breakChain boolean
// no processing will happen and the error will
// continue through the rejection path until done(,)


step(0) // 1
.catch(handleError)
.then(step) // 2
.catch(handleError)
.then(step) // 3
.catch(handleError)
.then(failingStep)  // 4 fail
.catch(handleError)
.then(step) // 5
.catch(handleError)
.then(step) // 6
.catch(handleError)
.done(function(){
log('success arguments', arguments);
}, function (error) {
log('Done, chain broke at step ' + error.step);
});

你会在控制台上看到:

step 1
step 2
step 3
step 4 (will fail)
errorHandler: caught error 'Failed on step 4'
errorHandler: skip handling
errorHandler: skip handling
Done, chain broke at step 4

这里有一些工作代码 Https://jsfiddle.net/8hzg5s7m/3/

如果您对每个步骤都有特定的处理,那么您的包装可以类似于:

/*
* simple wrapper to check if rejection
* has already been handled
* @param function real error handler
*/
function createHandler(realHandler) {
return function(error) {
if (error.breakChain) {
return q.reject(error);
}
realHandler(error);
error.breakChain = true;
return q.reject(error);
}
}

然后是你的锁链

step1()
.catch(createHandler(handleError1Fn))
.then(step2)
.catch(createHandler(handleError2Fn))
.then(step3)
.catch(createHandler(handleError3Fn))
.done(function(){
log('success');
}, function (error) {
log('Done, chain broke at step ' + error.step);
});

虽然有点晚了,但这个简单的解决方法对我很有效:

function chainError(err) {
return Promise.reject(err)
};


stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);

这使您可以脱离 休息的链条。

发现下面的 MDN 上的 Promise.prototype.catch()示例非常有帮助。

(公认的答案提到了 then(null, onErrorHandler),它与 catch(onErrorHandler)基本相同。)

捕获方法的使用和链接

var p1 = new Promise(function(resolve, reject) {
resolve('Success');
});


p1.then(function(value) {
console.log(value); // "Success!"
throw 'oh, no!';
}).catch(function(e) {
console.log(e); // "oh, no!"
}).then(function(){
console.log('after a catch the chain is restored');
}, function () {
console.log('Not fired due to the catch');
});


// The following behaves the same as above
p1.then(function(value) {
console.log(value); // "Success!"
return Promise.reject('oh, no!');
}).catch(function(e) {
console.log(e); // "oh, no!"
}).then(function(){
console.log('after a catch the chain is restored');
}, function () {
console.log('Not fired due to the catch');
});

投球失误

// Throwing an error will call the catch method most of the time
var p1 = new Promise(function(resolve, reject) {
throw 'Uh-oh!';
});


p1.catch(function(e) {
console.log(e); // "Uh-oh!"
});


// Errors thrown inside asynchronous functions will act like uncaught errors
var p2 = new Promise(function(resolve, reject) {
setTimeout(function() {
throw 'Uncaught Exception!';
}, 1000);
});


p2.catch(function(e) {
console.log(e); // This is never called
});


// Errors thrown after resolve is called will be silenced
var p3 = new Promise(function(resolve, reject) {
resolve();
throw 'Silenced Exception!';
});


p3.catch(function(e) {
console.log(e); // This is never called
});

如果能解决的话

//Create a promise which would not call onReject
var p1 = Promise.resolve("calling next");


var p2 = p1.catch(function (reason) {
//This is never called
console.log("catch p1!");
console.log(reason);
});


p2.then(function (value) {
console.log("next promise's onFulfilled"); /* next promise's onFulfilled */
console.log(value); /* calling next */
}, function (reason) {
console.log("next promise's onRejected");
console.log(reason);
});

最好的解决方案是重构到您的承诺链,使用 ES6 wait’s。然后您可以直接从函数返回以跳过其余的行为。

一年多以来,我一直在用我的头来对抗这种模式,使用 waiting’s 是一种天堂。

尝试像使用 libs 一样使用这个:

Https://www.npmjs.com/package/promise-chain-break

    db.getData()
.then(pb((data) => {
if (!data.someCheck()) {
tellSomeone();


// All other '.then' calls will be skiped
return pb.BREAK;
}
}))
.then(pb(() => {
}))
.then(pb(() => {
}))
.catch((error) => {
console.error(error);
});

如果希望使用异步/等待解决这个问题:

(async function(){
try {
const response1, response2, response3
response1 = await promise1()


if(response1){
response2 = await promise2()
}
if(response2){
response3 = await promise3()
}
return [response1, response2, response3]
} catch (error) {
return []
}


})()

使用序列承诺模块

意图

提供一个模块,其职责是顺序执行请求,同时按顺序跟踪每个操作的当前索引。在 命令模式中定义操作以获得灵活性。

参加者

  • Context : 其成员方法执行操作的对象。
  • Sequential誓言 : 定义一个 execute方法来链接和跟踪每个操作。
  • Invoker : 创建一个 SequentialNorman 实例,为其提供上下文和操作,并调用其 execute方法,同时为每个操作传入一个序号选项列表。

后果

当需要承诺解决方案的顺序行为时,使用 Sequential允诺。Sequential誓言将跟踪一个被拒绝的誓言的索引。

实施

clear();


var http = {
get(url) {
var delay = Math.floor( Math.random() * 10 ), even = !(delay % 2);
var xhr = new Promise(exe);


console.log(`REQUEST`, url, delay);
xhr.then( (data) => console.log(`SUCCESS: `, data) ).catch( (data) => console.log(`FAILURE: `, data) );


function exe(resolve, reject) {
var action = { 'true': reject, 'false': resolve }[ even ];
setTimeout( () => action({ url, delay }), (1000 * delay) );
}


return xhr;
}
};


var SequentialPromise = new (function SequentialPromise() {
var PRIVATE = this;


return class SequentialPromise {


constructor(context, action) {
this.index = 0;
this.requests = [ ];
this.context = context;
this.action = action;


return this;
}


log() {}


execute(url, ...more) {
var { context, action, requests } = this;
var chain = context[action](url);


requests.push(chain);
chain.then( (data) => this.index += 1 );


if (more.length) return chain.then( () => this.execute(...more) );
return chain;
}


};
})();


var sequence = new SequentialPromise(http, 'get');
var urls = [
'url/name/space/0',
'url/name/space/1',
'url/name/space/2',
'url/name/space/3',
'url/name/space/4',
'url/name/space/5',
'url/name/space/6',
'url/name/space/7',
'url/name/space/8',
'url/name/space/9'
];
var chain = sequence.execute(...urls);
var promises = sequence.requests;


chain.catch( () => console.warn(`EXECUTION STOPPED at ${sequence.index} for ${urls[sequence.index]}`) );


// console.log('>', chain, promises);


要点

顺序保证

如果在任何时候您返回 Promise.reject('something'),您将被抛出在 catch 块的承诺。

promiseOne
.then((result) => {
if (!result) {
return Promise.reject('No result');
}
return;
})
.catch((err) => {
console.log(err);
});

如果第一个承诺没有返回任何结果,那么在控制台中只能得到 “没有结果”