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



>更新:

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

问题:

我的目标似乎相当简单。

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)会被解雇,我理解这一点。我已经完成了所有事情,除了我想做的事情。

如何编写 promise 以便我可以在拒绝时调用函数,而无需调用错误链中的所有函数?还是有其他方法可以做到这一点?

这是一个现场演示,所以你可以做一些工作。

更新:

是解决了。在这里,我在链的末端捕获错误并将数据传递给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); 
}

你的代码没有按预期工作的原因是它实际上正在做一些与你认为的不同的事情。

假设您有类似以下内容的内容:

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 的 promise 库$q基于 kriskowal 的Q库(它具有更丰富的 API,但包含您可以在$q中找到的所有内容)。Q在GitHub上的API文档可能很有用。Q 实现了 Promises/A+ 规范,该规范详细介绍了then和承诺解析行为的确切工作原理。

编辑:

还要记住,如果你想在错误处理程序中脱离链,它需要返回一个被拒绝的承诺或抛出一个错误(它将被捕获并包装在被拒绝的承诺中)。如果不返回承诺,then会将返回值包装在解析承诺中。

这意味着,如果您不返回任何内容,则实际上是在返回值undefined的已解决承诺。

派对有点晚了,但这个简单的解决方案对我有用:

function chainError(err) {
return Promise.reject(err)
};
stepOne()
.then(stepTwo, chainError)
.then(stepThreee, chainError);

这使您可以脱离链条。

你需要的是一个重复的.then()链,有一个特殊情况开始,一个特殊案例结束。

诀窍是获取失败案例的步骤号,以波纹到最终的错误处理程序。

  • 开始:无条件拨打step(1)
  • 重复模式:使用以下回调链接.then()
    • 成功:呼叫步骤(N+1)
    • 失败:抛出拒绝上一个延迟的值或重新引发错误。
  • 完成:链接一个没有成功处理程序的.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处理。

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

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

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

这应该像将第一个承诺的失败情况更改为这样一样简单:

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

通过在第一步的失败情况下返回$q.reject(),您将拒绝该承诺,这会导致在第二then(...)中调用 errorCallback。

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

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

一年多来,我一直在反对这种模式,使用 await 就是天堂。

尝试像库一样使用它:

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/await 解决此问题:

(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 []
}
})()

如果在任何时候你返回Promise.reject('something')你将被扔进承诺的捕获块中。

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

如果第一个承诺没有返回任何结果,您只会在控制台中得到"无结果"。

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

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

注意:这基本上与pluma在他的答案中建议的模式相同,但使用了OP的命名。

在下面找到Promise.prototype.catch()MDN上的例子非常有帮助。

(接受的答案提到了then(null, onErrorHandler),这与catch(onErrorHandler)基本相同。

使用和链接 catch 方法

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

使用 SequentialPromise 模块

意图

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

参与者

  • 上下文:其成员方法执行操作的对象。
  • SequentialPromise:定义一个execute方法来链接和跟踪每个操作。SequentialPromise 从执行的所有操作中返回一个承诺链。
  • Invoker:创建一个 SequentialPromise 实例,为其提供上下文和操作,并在为每个操作传入选项的序号列表时调用其execute方法。

后果

当需要承诺解析的序号行为时,请使用顺序承诺。SequentialPromise 将跟踪 Promise 被拒绝的索引。

实现

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

要点

顺序承诺

最新更新