我提到,在JavaScript中编写异步代码时,我们有一些选择。
我想扩展这篇文章,多说一点关于你如何使用async/await、承诺和async返回承诺的能力的组合,以保持你的异步代码尽可能的干净。
编写异步代码(使用承诺)
假设你有一个传统的JavaScript函数,通过利用一个回调参数来操纵异步性。 我们想用async / await使其成为异步的。
// ===========================================================
// == This is the function that we want to make asynchronous
// == with async / await.
// ===========================================================
function getJobApplications (callback) {
database.getJobApplications((err, data) => {
callback(err, data)
})
}
在这个例子中,数据库函数getJobApplications 希望有一个回调,所以我们对此无能为力,说。但是,我们可以通过Promisifying ,使其成为异步的。
// ============================================================
// == In order to make this function asynchronous, we "promisify" it.
// == We inner-wrap it with a Promise so that the calling function can
// == wait for it to be resolved or rejected.
// ============================================================
function getJobApplications () {
return new Promise((resolve, reject) => {
database.getJobApplications((err, data) => {
if (err) return reject(err);
resolve(data);
})
})
}
这种Promisfying 你的函数的方法是很常见的。我们用一个承诺来包裹函数的内部,并手动指定函数何时应该resolve() (作为成功状态的结果)或reject() ,作为遇到错误的结果。
这很好......也很有效,但它不是很DRY(不要重复自己)。你可能很快就会为你想做成异步的所有事情写几个return new Promise((resolve,reject) => {}) 内部包装器。
只要你有一个依赖回调的函数,你想用await 来等待,你几乎总是要Promisify 该函数。
异步函数自动返回一个承诺
在其他情况下,当我们开始依赖越来越多的代码库使用async 函数时,一些美丽的事情开始发生。
这里有一个简单的例子。
async function sayHello () {
return "Hello";
}
sayHello().then(str => console.log(str)); // => Hello
因为异步函数的返回值总是一个承诺,即使你没有明确地返回一个承诺,你返回的值也会自动被包裹在一个承诺中。
下面是一个看起来更真实的例子。
async function getProfile (authToken, studentId) {
try {
let data = await Promise.all([
getJobApplications(studentId),
getAccountSettings(accountSettings),
getProfileDetails(studentId)
]);
// Implicitly wraps this return value in a promise
return {
apps: data[0],
account: data[1],
profile: data[2]
}
}
catch (err) {
// handle error
}
}
async function handleGetStudentProfile (req, res) {
let authToken = req.decoded.token;
let studentId = req.decoded.studentId;
try {
await checkIfAuthenticated(authToken);
let response = await getProfile(authToken, studentId)
return res.status(201).json(response)
}
catch (err) {
// Handle errors
}
}
突然间,事情的可读性提高了很多,需要Promisified 的代码层也减少了很多。
那错误呢?如果返回值总是被包裹在一个承诺中,那么当错误发生时,这意味着什么呢?
async function getProfile (authToken, studentId) {
try {
...
}
catch (err) {
// Error handling is simpler and looks a lot more like how it would
// in synchronous code.
return new Error(err)
}
}
我们可以像在同步语言中那样使用try/catch 块来捕捉它。这就更有可读性了。
用异步循环(再见IIFEs和ES5 Closures)
在ES6之前,在保持Iterable的当前值在范围内的情况下,循环处理异步的东西总是有点棘手的。
在ES5中,我们能够创建闭包来解决这个问题。
例如,如果我们想在学生档案中更新每个学生最喜欢的食物,让他们说喜欢香蕉,我们可以在这个ES5的例子中尝试这样的方法。
var promises = [];
for (var i = 0; i < students.length; i++) {
var student = student[i];
console.log(
`Updating student=${student.name} to say that they like bananas.
`);
promises.push(function() {
// If they don't already like bananas
if (student.favouriteFood !== "Bananas") {
// They'll like 'em now.
student.favouriteFood = "Bananas";
} else {
console.log('This student is cool, he already likes bananas :)');
}
return database.save(student);
})
}
Promise.all(promises)
.then((result) => {
// Done!
})
.catch((err) => {
// Handle error
})
但这并不奏效。
问题是,我们在循环的每次迭代中作为局部变量引入的student ,与我们在匿名函数中保存到承诺数组中的student ,是不一样的。
这主要是由于var 不是块范围的事实。
如果我们运行这个,我们会发现最后一个学生,在students.length - 1 ,被更新了students.length 次。
好吧,我们如何在ES5中解决这个问题呢?我们把这个块中我们想保持为一个块(不会改变)的部分封装成一个闭包。
var promises = [];
for (var i = 0; i < students.length; i++) {
var student = student[i];
console.log(
`Updating student=${student.name} to say that they like bananas.
`);
(function (student) {
promises.push(function() {
// If they don't already like bananas
if (student.favouriteFood !== "Bananas") {
// They'll like 'em now.
student.favouriteFood = "Bananas";
} else {
console.log('This student is cool, he already likes bananas :)');
}
return database.save(student);
})
})(student);
}
Promise.all(promises)
.then((result) => {
// Done!
})
.catch((err) => {
// Handle error
})
现在,我不想抛出太多的术语,但闭包是IIFEs,它将......立即评估。
如果我们这样做,我们就会把student 的当前值绑定到一个作用域上,在循环的每次迭代中,对学生的引用都不会改变。
好的,很好。但这样做很糟糕。如何在ES6中执行异步代码并将当前的局部变量绑定到块的作用域?
像这样...
var promises = [];
for (var i = 0; i < students.length; i++) {
const student = student[i]; // we could also use let instead of const
console.log(
`Updating student=${student.name} to say that they like bananas.
`);
promises.push(function() {
// If they don't already like bananas
if (student.favouriteFood !== "Bananas") {
// They'll like 'em now.
student.favouriteFood = "Bananas";
} else {
console.log('This student is cool, he already likes bananas :)');
}
return database.save(student);
})
}
Promise.all(promises)
.then((result) => {
// Done!
})
.catch((err) => {
// Handle error
})
const 和 ,与var不同,它们是块的作用域。 看看它们。let
然后使用我们在本文中学到的其他一些好东西,我们可以把这个函数清理得更干净。
async function updateStudents () {
for (let student of students) {
console.log(
`Updating student=${student.name} to say that they like bananas.
`);
if (student.favouriteFood !== "Bananas") {
// They'll like 'em now.
student.favouriteFood = "Bananas";
} else {
console.log('This student is cool, he already likes bananas :)');
}
await database.save(student);
}
}
updateStudents()
.then(() => {
// Go your merry way
})
.catch((err) => {
// There was a boo boo
})
现在,这是一个可读的地狱。
在你的浏览器中试试这个。看看你能不能猜到应该发生什么。
const students = ['josh', 'khalil'];
async function asyncTest () {
for (let student of students) {
await new Promise((resolve, reject) => {
setTimeout(() => {
console.log(student);
resolve();
}, 3000);
})
}
console.log('For loop completed')
}
asyncTest()
.then(() => {
console.log('Done!')
})
.catch((err) => {
console.log('An error', err)
})
它应该是打印出来的。
Promise {<pending>}
// wait 3 seconds
josh
// wait 3 seconds
khalil
For loop completed
Done!
我希望这篇文章对解释异步函数如何使你的代码更简洁有帮助如果你有任何问题或建议进行任何编辑,请随时在评论中留言。
现在开始构建吧,朋友们。