下面是一些简单的示例,说明如何对一个数组按顺序(一个接一个)执行每个异步操作进行排序。
假设您有一组项目:
var arr = [...];
并且,您希望对数组中的每个项目执行特定的异步操作,一次一个,以便下一个操作在前一个操作完成之前不会开始。
并且,假设您有一个用于处理数组中的一项的Promise返回函数fn(item)
:
手动迭代
function processItem(item) {
// do async operation and process the result
// return a promise
}
然后,您可以执行以下操作:
function processArray(array, fn) {
var index = 0;
function next() {
if (index < array.length) {
fn(array[index++]).then(next);
}
}
next();
}
processArray(arr, processItem);
手动迭代返回Promise
如果您想要返回一个PromiseprocessArray()
以便知道何时完成,您可以将其添加到其中:
function processArray(array, fn) {
var index = 0;
function next() {
if (index < array.length) {
return fn(array[index++]).then(function(value) {
// apply some logic to value
// you have three options here:
// 1) Call next() to continue processing the result of the array
// 2) throw err to stop processing and result in a rejected promise being returned
// 3) return value to stop processing and result in a resolved promise being returned
return next();
});
}
} else {
// return whatever you want to return when all processing is done
// this returne value will be the ersolved value of the returned promise.
return "all done";
}
}
processArray(arr, processItem).then(function(result) {
// all done here
console.log(result);
}, function(err) {
// rejection happened
console.log(err);
});
注意:这将在第一次拒绝时停止链并将该原因传递回 processArray 返回的Promise。
使用 .reduce() 进行迭代
如果你想用 Promise 做更多的工作,你可以链接所有的 Promise:
function processArray(array, fn) {
return array.reduce(function(p, item) {
return p.then(function() {
return fn(item);
});
}, Promise.resolve());
}
processArray(arr, processItem).then(function(result) {
// all done here
}, function(reason) {
// rejection happened
});
注意:这将在第一次拒绝时停止链并将该原因传递回从 返回的PromiseprocessArray()
。
对于成功场景,返回的PromiseprocessArray()
将使用fn
回调的最后一个已解析值进行解析。如果你想累积一个结果列表并用它来解决,你可以从一个闭包数组中收集结果,fn
并每次继续返回该数组,这样最终的解析将是一个结果数组。
用 .reduce() 迭代,用数组解决
而且,由于现在看来您希望最终的Promise结果是一个数据数组(按顺序),这里是对先前解决方案的修订,该解决方案产生了:
function processArray(array, fn) {
var results = [];
return array.reduce(function(p, item) {
return p.then(function() {
return fn(item).then(function(data) {
results.push(data);
return results;
});
});
}, Promise.resolve());
}
processArray(arr, processItem).then(function(result) {
// all done here
// array of data here in result
}, function(reason) {
// rejection happened
});
工作演示:http : //jsfiddle.net/jfriend00/h3zaw8u8/
以及显示拒绝的工作演示:http : //jsfiddle.net/jfriend00/p0ffbpoc/
使用 .reduce() 进行迭代,使用延迟解析数组
而且,如果您想在操作之间插入一个小的延迟:
function delay(t, v) {
return new Promise(function(resolve) {
setTimeout(resolve.bind(null, v), t);
});
}
function processArrayWithDelay(array, t, fn) {
var results = [];
return array.reduce(function(p, item) {
return p.then(function() {
return fn(item).then(function(data) {
results.push(data);
return delay(t, results);
});
});
}, Promise.resolve());
}
processArray(arr, 200, processItem).then(function(result) {
// all done here
// array of data here in result
}, function(reason) {
// rejection happened
});
使用 Bluebird Promise 库进行迭代
Bluebird promise 库内置了许多并发控制功能。例如,要对数组进行排序迭代,您可以使用Promise.mapSeries()
.
Promise.mapSeries(arr, function(item) {
// process each individual item here, return a promise
return processItem(item);
}).then(function(results) {
// process final results here
}).catch(function(err) {
// process array here
});
或者在迭代之间插入延迟:
Promise.mapSeries(arr, function(item) {
// process each individual item here, return a promise
return processItem(item).delay(100);
}).then(function(results) {
// process final results here
}).catch(function(err) {
// process array here
});
使用 ES7 异步/等待
如果您在支持 async/await 的环境中编码,您也可以只使用常规for
循环,然后await
在循环中使用 promise,这将导致for
循环暂停,直到在继续之前解决 promise。这将有效地对您的异步操作进行排序,因此在前一个操作完成之前,下一个操作不会开始。
async function processArray(array, fn) {
let results = [];
for (let i = 0; i < array.length; i++) {
let r = await fn(array[i]);
results.push(r);
}
return results; // will be resolved value of promise
}
// sample usage
processArray(arr, processItem).then(function(result) {
// all done here
// array of data here in result
}, function(reason) {
// rejection happened
});
仅供参考,我认为我processArray()
在这里的函数与Promise.map()
Bluebird promise 库中的函数非常相似,它接受一个数组和一个 promise 生成函数,并返回一个使用已解析结果数组进行解析的 promise。
@vitaly-t - 这里有一些关于你的方法的更详细的评论。欢迎您使用任何您认为最好的代码。当我第一次开始使用 Promise 时,我倾向于只将 Promise 用于他们所做的最简单的事情,并且当更高级的 Promise 使用可以为我做更多的事情时,我自己编写了很多逻辑。你只使用你完全熟悉的东西,除此之外,你更愿意看到你自己熟悉的代码。这大概就是人性吧。
我会建议,随着我对 Promise 可以为我做什么的了解越来越多,我现在喜欢编写使用更多 Promise 高级功能的代码,这对我来说似乎很自然,我觉得我正在构建良好的基础经过测试的基础设施具有许多有用的功能。我只要求你在学习越来越多的知识时保持开放的心态,以便朝着那个方向前进。我认为,随着您的理解提高,迁移是一个有用且富有成效的方向。
以下是对您的方法的一些具体反馈意见:
你在七个地方创建Promise
作为风格的对比,我的代码只有两个地方我显式地创建了一个新的Promise - 一次在工厂函数中,一次用于初始化.reduce()
循环。在其他地方,我只是通过链接到它们或返回其中的值或直接返回它们来构建已经创建的Promise。您的代码有七个独特的地方,您可以在其中创建Promise。现在,好的编码不是看你能在多少地方创建Promise的比赛,但这可能表明利用已经创建的Promise与测试条件和创建新Promise的差异。
投掷安全是一个非常有用的功能
Promise 是抛出安全的。这意味着Promise处理程序中抛出的异常将自动拒绝该Promise。如果你只是想让异常变成拒绝,那么这是一个非常有用的特性。事实上,你会发现直接抛出自己是一种从处理程序中拒绝而不创建另一个Promise的有用方法。
很多Promise.resolve()
或Promise.reject()
可能是简化的机会
如果您看到包含大量Promise.resolve()
orPromise.reject()
语句的代码,那么可能有机会更好地利用现有的 Promise,而不是创建所有这些新的 Promise。
投向Promise
如果您不知道某些东西是否返回了Promise,那么您可以将其转换为Promise。然后,promise 库会自己检查它是否是一个promise,甚至它是否与您正在使用的promise 库相匹配,如果不是,则将其包装成一个。这样可以省去自己重写很多这样的逻辑。
返回Promise的合同
在如今的许多情况下,为可能执行异步操作以返回Promise的函数签订合同是完全可行的。如果函数只是想做一些同步的事情,那么它可以只返回一个已解决的Promise。你似乎觉得这很繁重,但这绝对是风吹草动的方式,我已经写了很多需要这样做的代码,一旦你熟悉了 promises 就会感觉很自然。它抽象出操作是同步还是异步,并且调用者不必知道或以任何方式做任何特殊的事情。这是 Promise 的一个很好的用法。
可以编写工厂函数来仅创建一个Promise
可以编写工厂函数来仅创建一个Promise,然后解决或拒绝它。这种风格还使其安全抛出,因此工厂函数中发生的任何异常都会自动成为拒绝。它还使合同始终自动返回Promise。
虽然我意识到这个工厂函数是一个占位符函数(它甚至不做任何异步操作),但希望你能看到考虑它的风格:
function factory(idx) {
// create the promise this way gives you automatic throw-safety
return new Promise(function(resolve, reject) {
switch (idx) {
case 0:
resolve("one");
break;
case 1:
resolve("two");
break;
case 2:
resolve("three");
break;
default:
resolve(null);
break;
}
});
}
如果这些操作中的任何一个是异步的,那么他们可以只返回自己的Promise,这些Promise会自动链接到一个中央Promise,如下所示:
function factory(idx) {
// create the promise this way gives you automatic throw-safety
return new Promise(function(resolve, reject) {
switch (idx) {
case 0:
resolve($.ajax(...));
case 1:
resole($.ajax(...));
case 2:
resolve("two");
break;
default:
resolve(null);
break;
}
});
}
return promise.reject(reason)
不需要使用拒绝处理程序
当您拥有这段代码时:
return obj.then(function (data) {
result.push(data);
return loop(++idx, result);
}, function (reason) {
return promise.reject(reason);
});
拒绝处理程序没有添加任何值。您可以改为这样做:
return obj.then(function (data) {
result.push(data);
return loop(++idx, result);
});
您已经在返回 的结果obj.then()
。如果obj
拒绝或者任何链接到obj
或从 then.then()
处理程序返回的内容拒绝,obj
则将拒绝。所以你不需要用拒绝创建一个新的Promise。没有拒绝处理程序的更简单的代码用更少的代码做同样的事情。
这是您的代码的一般架构中的一个版本,它试图结合大多数这些想法:
function factory(idx) {
// create the promise this way gives you automatic throw-safety
return new Promise(function(resolve, reject) {
switch (idx) {
case 0:
resolve("zero");
break;
case 1:
resolve("one");
break;
case 2:
resolve("two");
break;
default:
// stop further processing
resolve(null);
break;
}
});
}
// Sequentially resolves dynamic promises returned by a factory;
function sequence(factory) {
function loop(idx, result) {
return Promise.resolve(factory(idx)).then(function(val) {
// if resolved value is not null, then store result and keep going
if (val !== null) {
result.push(val);
// return promise from next call to loop() which will automatically chain
return loop(++idx, result);
} else {
// if we got null, then we're done so return results
return result;
}
});
}
return loop(0, []);
}
sequence(factory).then(function(results) {
log("results: ", results);
}, function(reason) {
log("rejected: ", reason);
});
工作演示:http : //jsfiddle.net/jfriend00/h3zaw8u8/
关于这个实现的一些评论:
Promise.resolve(factory(idx))
基本上将结果factory(idx)
转换为Promise。如果它只是一个值,那么它就变成了一个以返回值作为解析值的已解析Promise。如果它已经是一个Promise,那么它只是链接到那个Promise。因此,它替换了factory()
函数返回值上的所有类型检查代码。
工厂函数通过返回null
其解析值最终为或 的Promise来表示它已完成null
。上面的转换将这两个条件映射到相同的结果代码。
工厂函数自动捕获异常并将它们转换为拒绝,然后由sequence()
函数自动处理。如果您只想中止处理并将错误反馈给第一个异常或拒绝,那么这是让 Promise 进行大量错误处理的一个显着优势。
此实现中的工厂函数可以返回Promise或静态值(对于同步操作),并且它会正常工作(根据您的设计要求)。
我已经在工厂函数的Promise回调中使用抛出的异常对其进行了测试,它确实只是拒绝并将该异常传播回以拒绝以异常为原因的序列Promise。
这使用与您类似的方法(有意尝试保持您的一般架构)将多个调用链接到loop()
.