这篇文章很可能是概念性的,因为我首先从很多伪代码开始。 - 最后你会看到这个问题的用例,虽然解决方案将是一个“我可以添加到我的工具带中的有用编程技术工具”。
有时可能需要创建多个承诺,并在所有承诺结束后执行某些操作。或者根据先前承诺的结果,可以创建多个承诺。可以类推,创建一个值数组而不是单个值。 有两种基本情况需要考虑,其中承诺的数量与所述承诺的结果不一致,以及它是依赖的情况。什么“可以”完成的简单伪代码。
for (let i=0; i<10; i++) {
promise(...)
.then(...)
.catch(...);
}.then(new function(result) {
//All promises finished execute this code now.
})
基本上创建了n(10)个promise,最终的代码将在所有promise完成后执行。当然语法在javascript中不起作用,但它显示了这个想法。这个问题相对简单,可以称为完全异步。
现在第二个问题是:
while (continueFn()) {
promise(...)
.then(.. potentially changing outcome of continueFn ..)
.catch(.. potentially changing outcome of continueFn ..)
}.then(new function(result) {
//All promises finished execute this code now.
})
这要复杂得多,因为人们不能只是开始所有的承诺,然后等待它们完成:最终你必须“承诺承诺”。第二种情况是我想弄清楚的(如果可以做第二种情况,你也可以做第一种情况)。
我确实有一个有效的“解决方案”。这不是一个很好的解决方案,因为可能很快就会看到,在代码之后我将讨论为什么我不喜欢这种方法。基本上它不使用循环而是使用递归 - 所以“promise”(或承诺的包装器是一个承诺)在代码中实现时会自动调用:
function promiseFunction(state_obj) {
return new Promise((resolve, reject) => {
//initialize fields here
let InnerFn = (stateObj) => {
if (!stateObj.checkContinue()) {
return resolve(state_obj);
}
ActualPromise(...)
.then(new function(result) {
newState = stateObj.cloneMe(); //we'll have to clone to prevent asynchronous write problems
newState.changeStateBasedOnResult(result);
return InnerFn(newState);
})
.catch(new function(err) {
return reject(err); //forward error handling (must be done manually?)
});
}
InnerFn(initialState); //kickstart
});
}
需要注意的重要一点是,stateObj
在其生命周期内不应该改变,但它可以非常简单。在我真正的问题中(我将在最后解释),stateObj只是一个计数器(数字),而if (!stateObj.checkContinue())
只是if (counter < maxNumber)
。
现在这个解决方案非常糟糕;它很丑陋,复杂,容易出错,最终无法扩展。 丑陋,因为实际的业务逻辑埋藏在一堆乱七八糟的代码中。它没有显示“在罐头上”实际上只是做上面的while循环所做的事情。 因为执行流程无法遵循而变得复杂。首先,递归代码永远不会“容易”遵循,但更重要的是,您还必须牢记使用状态对象的线程安全性。 (也可能有另一个对象的引用,比如存储结果列表以供以后处理)。 由于存在冗余而非严格必要,因此容易出错;你必须明确转发拒绝。诸如堆栈跟踪之类的调试工具也很快变得难以查看。 可伸缩性在某些方面也是一个问题:这是一个递归函数,因此在某一点上它将创建一个stackoverflow /遇到最大递归深度。通常,可以通过尾递归进行优化,或者更常见的是,在堆上创建虚拟堆栈,并使用手动堆栈将函数转换为循环。但是,在这种情况下,无法将递归调用更改为带有手动堆栈的循环;仅仅是因为承诺语法如何工作。
一位同事提出了解决这个问题的另一种方法,这种方法最初看起来不那么成问题,但是我放弃了最终的方法,因为它违背了所有承诺的意图。
他所建议的基本上是按照上面的承诺循环。但是不是让循环继续,而是会有一个变量“finished”和一个不断检查这个变量的内部循环;所以在代码中它会是这样的:
function promiseFunction(state_obj) {
return new Promise((resolve, reject) => {
while (stateObj.checkContinue()) {
let finished = false;
let err = false;
let res = null;
actualPromise(...)
.then(new function(result) {
res = result;
finished = true;
})
.catch(new function(err) {
res = err;
err = true;
finished = true;
});
while(!finished) {
sleep(100); //to not burn our cpu
}
if (err) {
return reject(err);
}
stateObj.changeStateBasedOnResult(result);
}
});
}
虽然这不那么复杂,但现在很容易遵循执行流程。这有它自己的问题:至少不清楚这个功能什么时候结束;这对于表现来说真的很糟糕。
那么这个结论还不多,我真的很喜欢上面第一个伪代码中的简单内容。也许是另一种看待事物的方式,这样就不会有深度递归函数的麻烦。
那么你如何重写作为循环一部分的承诺呢?
现在这个问题源于我必须创造的真实事物。虽然现在已经解决了这个问题(通过应用上面的递归方法),但知道产生这个问题的内容可能会很有趣;然而,真正的问题不是关于这个具体的案例,而是关于如何在任何承诺下做到这一点。
在sails应用程序中,我必须检查一个数据库,该数据库包含订单ID。我必须找到第一个N“不存在的订单 - ID”。我的解决方案是从数据库中获取“第一”M产品,找到其中缺少的数字。然后,如果缺失的数量小于N,则获得下一批M产品。
现在要从数据库中获取项目,使用promise(或回调),因此代码不会等待数据库数据返回。 - 所以我基本上处于“第二个问题:”
function GenerateEmptySpots(maxNum) {
return new Promise((resolve, reject) => {
//initialize fields
let InnerFn = (counter, r) => {
if (r > 0) {
return resolve(true);
}
let query = {
orderNr: {'>=': counter, '<': (counter + maxNum)}
};
Order.find({
where: query,
sort: 'orderNr ASC'})
.then(new function(result) {
n = findNumberOfMissingSpotsAndStoreThemInThis();
return InnerFn(newState, r - n);
}.bind(this))
.catch(new function(err) {
return reject(err);
});
}
InnerFn(maxNum); //kickstart
});
}
sleep
function in the alternative is just from another library which provided a non-blocking-sleep. (not that it matters).替代(坏)解决方案
...实际上并不起作用,因为JavaScript中没有sleep
函数。 (如果你有一个提供非阻塞睡眠的运行时库,你可以使用while
循环和非阻塞 - 使用相同的样式在其中等待承诺)。
糟糕的解决方案是丑陋,复杂,容易出错,最终无法扩展。
不。递归方法确实是执行此操作的正确方法。
丑陋,因为实际的业务逻辑埋藏在一堆乱七八糟的代码中。并且容易出错,因为您必须明确转发拒绝。
这只是由Promise
constructor antipattern造成的!躲开它。
因为执行流程无法遵循而变得复杂。递归代码永远不会“容易”遵循
我会质疑这个说法。你必须习惯它。
您还必须记住状态对象的线程安全性。
不。在JavaScript中没有多线程和共享内存访问,如果你担心并发,其他东西会影响你的状态对象,而循环运行会导致任何方法出现问题。
可扩展性在某些方面也是一个问题:这是一个递归函数,所以在某一点上它会创建一个stackoverflow
不,这是异步的!回调将在一个新的堆栈上运行,它实际上并不是在函数调用期间递归调用的,并且不会携带这些堆栈帧。异步事件循环已经提供了trampoline来实现这种尾递归。
function promiseFunction(state) {
const initialState = state.cloneMe(); // clone once for this run
// initialize fields here
return (function recurse(localState) {
if (!localState.checkContinue())
return Promise.resolve(localState);
else
return actualPromise(…).then(result =>
recurse(localState.changeStateBasedOnResult(result))
);
}(initialState)); // kickstart
}
你知道,async
/ await
可用于实现ES6的每个环境,因为它们现在都实现了ES8!
async function promiseFunction(state) {
const localState = state.cloneMe(); // clone once for this run
// initialize fields here
while (!localState.checkContinue()) {
const result = await actualPromise(…);
localState = localState.changeStateBasedOnResult(result);
}
return localState;
}
让我们从简单的案例开始:你有N个承诺,所有人都做了一些工作,并且你想在所有承诺完成后做点什么。实际上有一种内置方式可以做到这一点:Promise.all
。有了它,代码将如下所示:
let promises = [];
for (let i=0; i<10; i++) {
promises.push(doSomethingAsynchronously());
}
Promise.all(promises).then(arrayOfResults => {
// all promises finished
});
现在,第二次调用是一种情况,当你想要继续异步地执行某些操作时,这取决于先前的异步结果。一个常见的例子(有点不那么抽象)就是简单地获取页面,直到你结束。
使用现代JavaScript,幸运的是以一种非常易读的方式编写它:使用asynchronous functions和await
:
async function readFromAllPages() {
let shouldContinue = true;
let pageId = 0;
let items = [];
while (shouldContinue) {
// fetch the next page
let result = await fetchSinglePage(pageId);
// store items
items.push.apply(items, result.items);
// evaluate whether we want to continue
if (!result.items.length) {
shouldContinue = false;
}
pageId++;
}
return items;
}
readFromAllPages().then(allItems => {
// items have been read from all pages
});
如果没有async / await,这看起来会有点复杂,因为你需要自己管理所有这些。但除非你试图让它超级通用,否则看起来不应该那么糟糕。例如,分页可能如下所示:
function readFromAllPages() {
let items = [];
function readNextPage(pageId) {
return fetchSinglePage(pageId).then(result => {
items.push.apply(items, result.items);
if (!result.items.length) {
return Promise.resolve(null);
}
return readNextPage(pageId + 1);
});
}
return readNextPage(0).then(() => items);
}
首先,递归代码永远不会“容易”遵循
我认为代码很好读。正如我所说:除非你试图让它超级通用,否则你可以保持简单。命名也有很大帮助。
但更重要的是,您还必须牢记使用状态对象的线程安全性
不,JavaScript是单线程的。你是异步做事但这并不一定意味着事情同时发生。 JavaScript使用事件循环来处理异步进程,其中一次只运行一个代码块。
可伸缩性在某些方面也是一个问题:这是一个递归函数,因此在某一点上它将创建一个stackoverflow /遇到最大递归深度。
也没有。在函数引用自身的意义上,这是递归的。但它不会直接称自己为自己。相反,当异步进程完成时,它会将自身注册为回调。因此,函数的当前执行将首先完成,然后在某个时刻异步进程完成,然后回调最终将运行。这些是(至少)与事件循环分开的三个步骤,它们都独立于另一个循环运行,所以这里没有递归深度的问题。
事情的关键似乎是“实际的业务逻辑被埋在一堆乱码”中。
是的,在两种解决方案中都是......
事情可以分开:
asyncRecursor
函数,只知道如何(异步)递归。允许调用者负责克隆原始对象而不是resolver()
,假设克隆始终是必要的。在这方面,来电者确实需要负责。
function asyncRecursor(subject, testFn, workFn) {
// asyncRecursor orchestrates the recursion
if(testFn(subject)) {
return Promise.resolve(workFn(subject)).then(result => asyncRecursor(result, testFn, workFn));
// the `Promise.resolve()` wrapper safeguards against workFn() not being thenable.
} else {
return Promise.resolve(subject);
// the `Promise.resolve()` wrapper safeguards against `testFn(subject)` failing at the first call of asyncRecursor().
}
}
现在您可以按如下方式编写调用者:
// example caller
function someBusinessOrientedCallerFn(state_obj) {
// ... preamble ...
return asyncRecursor(
state_obj, // or state_obj.cloneMe() if necessary
(obj) => obj.checkContinue(), // testFn
(obj) => somethingAsync(...).then((result) => { // workFn
obj.changeStateBasedOnResult(result);
return obj; // return `obj` or anything you like providing it makes a valid parameter to be passed to `testFn()` and `workFn()` at next recursion.
});
);
}
理论上你可以将你的终端测试整合到workFn
中,但保持它们分开将有助于在业务逻辑的编写者中强制执行纪律,以记住包含测试。否则他们会认为它是可选的,你可以随心所欲,他们会把它留下来!
对不起,这不使用Promises,但有时抽象只是妨碍了。
这个例子是根据@ poke的答案构建的,简短易懂。
function readFromAllPages(done=function(){}, pageId=0, res=[]) {
fetchSinglePage(pageId, res => {
if (res.items.length) {
readFromAllPages(done, ++pageId, items.concat(res.items));
} else {
done(items);
}
});
}
readFromAllPages(allItems => {
// items have been read from all pages
});
这只有一个深度的嵌套函数。通常,您可以解决嵌套回调问题,而无需使用为您管理事物的子系统。
如果我们删除参数默认值并更改箭头函数,我们将获得在旧版ES3浏览器中运行的代码。