嘘!我正在使用一些 ES6 语法,请至少在 Node 6 上运行代码片段,好吗?
异步任务可以建模为一个接受回调的函数:
function task(arg1, arg2, callback) {
// ...
callback(null, result);
}
task(arg1, arg2, (err, result) => {
handle(result);
});
但是有一个替代约定通常可以简化事情:
function task(arg1, arg2) {
// ...
return Promise.resolve(result);
}
task(arg1, arg2).then(handle(result));
虽然这两种约定都有意义,但我发现第二种方法在实践中更有助于编写具有良好错误处理能力的简单异步代码。
要掌握的最重要的几点是:
- 函数返回一个值;异步任务返回一个未来值的承诺。
- 当异步任务完成时,它会将承诺标记为“已解决”。
- 如果异步任务失败,它不会引发异常,而是将承诺与错误一起标记为“被拒绝”。
- 返回的 Promise 也是一个对象。你甚至可以在它完成之前用它做有用的事情。运行异步任务的代码不必与对任务结果感兴趣的代码位于同一位置。
与回调不同,promise 的一个重要方面是保证它们是异步的:
// callbacks
myTask(1, 2, (err, result) => {
console.log("A");
});
console.log("B");
// can be AB or BA depending on myTask
// promises
myTask(1, 2).then(() => {
console.log("A");
})
console.log("B");
// always BA
这使代码更容易推理,但这也意味着当您实际上依赖于第二种行为时,promise 将无济于事。
(阅读更多关于 Promises 的内容!)
初始点
好的,让我们回到你的代码。首先让我ogs
用一个虚拟的异步函数替换,这样我们就可以在没有网络的情况下处理一些代码:
var async = require('async');
function ogs(param, callback) {
let value = ["ogs", param];
setTimeout(
() => callback(null, value),
20);
}
// global variables
var param1 = {url: 'http://www.google.com/'};
var param2 = {url: 'https://www.yahoo.com/'};
function function1(callback){
ogs(param1, function(error, data1) {
callback(null, data1);
});
}
function function2(data1, callback){
ogs(param2, function(error, data2) {
callback(null, data1, data2);
});
}
function function3(data1, data2, callback){
console.log(data1);
console.log("---------------");
console.log(data2);
}
(function temp() {
async.waterfall([function1, function2, function3],
function(err, result){
console.log(result);
console.log(err);
if(err) console.log(err);
}
);
})();
让我们试试这些承诺的事情
一个等价的ogs
返回一个承诺而不是一个回调可能看起来像:
function ogs(param, callback) {
// return a promise that resolves after 20ms
return new Promise((resolve, reject) => {
setTimeout(() => {
let value = ["ogs", param];
resolve(value);
}, 20);
});
}
因为ogs
now 返回了一个 promise,所以在 each 中使用它很简单function
:
function function1(){
return ogs(param1); // call async task, obtain the promise for its result and return it directly
}
function function2() {
return ogs(param2);
}
function function3(data1, data2){
console.log(data1);
console.log("---------------");
console.log(data2);
}
如果您想在中间添加一些日志记录,这也很容易:
function function2() {
return ogs(param2).then(data2 => {
console.log("inside function2", data2);
return data2;
});
}
现在每个步骤都是一个返回承诺的异步任务,让我们将它们连接在一起!最简单的方法是Promise.then
直接使用:
(function temp() {
function1().then(data1 => {
return function2().then(data2 => {
return function3(data1, data2);
});
}).catch(error => {
console.error("There was a problem:", error);
})
})();
这将运行function1
,完成后,会将结果传递给function2
,然后将两个结果都传递给function3
。
并行运行
可是等等!function2
甚至不需要等待function1
完成。这是两个单独的请求。我们可以同时启动它们。
(function temp() {
let data1Promise = function1();
let data2Promise = function2();
Promise.all([data1Promise, data2Promise]).then(([data1, data2]) => {
return function3(data1, data2);
}).catch(error => {
console.error("There was a problem:", error);
})
})();
Promise.all
接受一组承诺并返回一个用一组结果解析的承诺。我们从数组中解压缩这些结果并将它们传递给function3
.
并行运行网络请求应该可以让您的应用程序运行得更快。赢!
现在回到你原来的问题:
如何摆脱全局变量?
我们可以完全控制 and 的签名function1
,function2
所以让我们使用它吧!让我们让这些函数将参数作为参数,而不是查看全局变量。像这样:
function function1(param){
return ogs(param);
}
function function2(param) {
return ogs(param, {"some other options": true});
}
这些功能现在看起来超级相似!也许您可以只使用一个(或者只是放下它们并ogs
直接打电话?)
删除全局变量后,我们的代码现在如下所示:
(function temp() {
let param1 = {url: 'http://www.google.com/'};
let param2 = {url: 'https://www.yahoo.com/'};
let data1Promise = function1(param1);
let data2Promise = function2(param2);
Promise.all([data1Promise, data2Promise]).then(([data1, data2]) => {
return function3(data1, data2);
}).catch(error => {
console.error("There was a problem:", error);
})
})();
但我真的需要我的函数按顺序运行!
如果function2
没有结果实际上无法启动function1
怎么办?
function function1(param) {
return ogs(param);
}
function function2(data1, param) {
return ogs(param2, {"some other options": data1});
}
我们可以使用嵌套恢复到第一个版本,then
但我们也可以尝试更整洁的东西:
(function temp() {
let param1 = {url: 'http://www.google.com/'};
let param2 = {url: 'https://www.yahoo.com/'};
let data1Promise = function1(param1);
let data2Promise = data1Promise.then(data1 => function2(data1, param2)); // !
Promise.all([data1Promise, data2Promise]).then(([data1, data2]) => {
return function3(data1, data2);
}).catch(error => {
console.error("There was a problem:", error);
})
})();
它与 有何不同async.waterfall
?
waterfall
要求您以这样一种方式编写函数,以便它们调用callback
下一步所需的所有信息。流程如下所示:
function1
-> (data1)
function2
-> (data1, data2)
function3
想象一下,如果您必须链接 10 个调用而不是 2 个......基本上第 2 步需要知道第 3、4、5、6 步可能需要什么。
使用 Promise,您可以通过从每个任务返回一个数组来做同样的事情,但您可以做得更好:
不再需要ogs
用function1
andfunction2
包装,因为你可以这样做:
Promise.all([ogs(...), ogs(...), ogs(...)]).then(allResults)
一切都为您收集在一个数组中。
非常相关的阅读:当一个承诺依赖于另一个承诺时,Bluebird 的 Promise.all() 方法
但是我的 API 没有返回承诺!
我希望我现在已经让你加入了承诺,但你仍然坚持这个签名:
ogs(options, function (err, results) {...})
我们想把它转换成类似的东西:
ogsAsync(options) -> Promise
使用 Promise 构造函数手动完成很简单:
function ogsAsync(options) {
return new Promise((resolve, reject) => {
ogs(options, (err, results) => {
if (err) {
reject(err);
} else {
resolve(results);
}
});
});
}
但是你可能不需要,因为看起来你的库已经返回了一个 promise,所以你可以osg(options)
直接调用——它已经返回了一个 promise。耶!
但是以防万一你必须使用一个还没有提供承诺的库(就像redis
或大部分node
标准库一样),Bluebird提供了一个很好的实用程序来自动将回调风格的任务包装到承诺风格的任务中。
希望有帮助!