Javascript “更好的方法”;“循环”;承诺
这篇文章可能会给人一种相当概念化的印象,因为我第一次开始时有很多伪代码。-最后,您将看到这个问题的用例,尽管解决方案是“我可以添加到有用编程技术的工具带中的工具” 问题 有时,一个人可能需要创建多个承诺,或者在所有承诺都结束后做一些事情。或者,可以根据先前承诺的结果创建多个承诺。类似于创建一个值数组而不是单个值。Javascript “更好的方法”;“循环”;承诺,javascript,recursion,ecmascript-6,promise,Javascript,Recursion,Ecmascript 6,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(计数器
现在这个解决方案真的很糟糕;它丑陋、复杂、容易出错,最终无法扩展。丑陋是因为实际的业务逻辑隐藏在混乱的代码中。它不会显示“在can上”实际上只是执行上面while循环所做的操作。
复杂是因为执行流程无法遵循。首先,递归代码从来都不容易理解,但更重要的是,您还必须记住状态对象的线程安全性。(也可能有对另一个对象的引用,例如,存储一个结果列表以供以后处理)。
它容易出错,因为冗余比严格要求的要多;您必须明确地转发拒绝。调试工具(如堆栈跟踪)也很快变得很难查看。
在某些情况下,可伸缩性也是一个问题:这是一个递归函数,因此在某一点上,它将创建一个stackoverflow/遇到最大递归深度。通常,可以通过尾部递归进行优化,或者更常见的是,创建一个虚拟堆栈(在堆上),并使用手动堆栈将函数转换为循环。然而,在这种情况下,不能将递归调用更改为具有手动堆栈的循环;这仅仅是因为promise语法是如何工作的 替代(坏)解决方案 一位同事提出了解决这个问题的另一种方法,这种方法最初看起来问题不大,但我最终放弃了,因为它违背了承诺要做的一切 他建议的基本上是按照上面的承诺循环。但不是让循环继续,而是有一个变量“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”。我的解决方案是从数据库中获取“first”M产品,找出其中缺少的数字。然后,如果缺失数量小于N,则获得下一批M产品
现在要从数据库中获取一个项目,需要使用承诺(或回调),因此代码不会等待数据库数据返回。-所以我基本上处于“第二个问题”:
函数GenerateEmptySpots(maxNum){
返回新承诺((解决、拒绝)=>{
//初始化字段
让InnerFn=(计数器,r)=>{
如果(r>0){
返回resolve(true);
}
让查询={
orderNr:{'>=':计数器,'
替代(坏)解决方案
…实际上不起作用,因为JavaScript中没有sleep
函数。(如果您有一个提供非阻塞睡眠的运行时库,您可以使用,而循环和非阻塞使用相同的样式在其中等待承诺)
糟糕的解决方案丑陋、复杂、容易出错,最终无法扩展
不,递归方法确实是正确的方法
丑陋是因为实际的业务逻辑隐藏在一堆乱七八糟的代码中,而且容易出错,因为您必须显式地转发拒绝
这仅仅是由…引起的!避免它
复杂是因为执行流程不可能遵循。递归代码从来都不“容易”遵循
我将对这一说法提出质疑。
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
});
}
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);
}
});
}
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
});
}
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 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;
}
let promises = [];
for (let i=0; i<10; i++) {
promises.push(doSomethingAsynchronously());
}
Promise.all(promises).then(arrayOfResults => {
// all promises finished
});
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
});
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);
}
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
});
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.
});
);
}