深入理解ES7 async/await用法:从Promise到异步编程演进
本文深入解析ES7 async/await语法,对比Promise链与co模块的优劣。通过MongoDB查询示例,展示如何消除回调地狱,实现更优雅的异步代码。涵盖异常处理、并行执行及循环await等核心用法,帮助开发者掌握现代JavaScript异步编程技巧。
在早期学习ES6 Promise时,曾撰写博文《promise和co搭配生成器函数方式解决js代码异步流程的比较》,对比了利用Promise与co模块结合生成器函数处理JavaScript异步流程的异同。
该文章末尾简要提及了ES7的async与await,但当时仅做简单提及,未作深入探讨。
随着Node.js V7发布,已正式支持async与await。本文将对此进行深入研究,旨在以更优雅的方式编写异步代码。
async/await是什么
async/await是什么
async/await可视为co模块与生成器函数的语法糖,旨在通过更清晰的语义解决JavaScript异步代码问题。
熟悉co模块的读者知道,co模块由TJ大神开发,用于执行生成器函数以处理异步流程。async/await则是其升级版,内置了生成器函数的执行器,不再依赖co模块,且async函数返回的是Promise对象。
无论是co模块还是async/await,均以Promise为基础单元。对Promise了解不深的读者,建议先深入学习Promise相关知识。
对比Promise,co,async/await
对比Promise,co,async/await
下面通过一个简单示例,对比三种方式的异同及取舍策略。
示例采用Node.js驱动查询MongoDB数据库,原因是该驱动默认返回Promise,无需额外包装。
使用Promise链
MongoClient.connect(url + db_name).then(db=> {
return db.collection('blogs');
}).then(coll=> {
return coll.find().toArray();
}).then(blogs=> {
console.log(blogs.length);
}).catch(err=> {
console.log(err);
})
Promise的then()方法可返回另一个Promise或同步值;若返回同步值,将被自动包装为Promise。
上述示例中,db.collection()返回同步值(集合对象),但被包装为Promise后,会透传至下一个then()方法。
上述示例展示了Promise链的使用方式。
流程为:先通过MongoClient.connect()连接数据库返回Promise,在then()中获取数据库对象db,再获取coll对象并返回;随后在下一个then()中获取coll对象进行查询,逐层调用then()形成Promise链。
若Promise链中任一环节出现异常,均可被最后的catch()捕获。
相比层层调用回调函数,这种Promise链写法更为优雅,流程明确:先获取数据库对象,再获取集合对象,最后查询数据。
然而,存在一个不够“优雅”的问题:每个then()获取的对象仅来自上一个then()返回的数据,无法跨层访问。
这意味着在第三个then(blogs => {})中,只能获取查询结果blogs,无法直接使用上方的db和coll对象。若需在打印blogs列表后关闭数据库db.close(),该如何处理?
此时有两种解决方法:
第一种是使用then()嵌套。我们将Promise链打断并嵌套,类似回调函数的嵌套方式:
MongoClient.connect(url + db_name).then(db=> {
let coll = db.collection('blogs');
coll.find().toArray().then(blogs=> {
console.log(blogs.length);
db.close();
}).catch(err=> {
console.log(err);
});
}).catch(err=> {
console.log(err);
})
此处将两个Promise嵌套,使得在最后一个查询操作中可调用外部的db对象。但此方式不推荐,原因很简单:我们从一种回调地狱走向了另一种Promise回调地狱。
此外,由于Promise未形成链,需对每个Promise的异常进行单独捕捉。
另一种方式是在每个then()方法中将db对象传递过来:
MongoClient.connect(url + db_name).then(db=> {
return {db:db,coll:db.collection('blogs')};
}).then(result=> {
return {db:result.db,blogs:result.coll.find().toArray()};
}).then(result=> {
return result.blogs.then(blogs=> { //注意这里,result.coll.find().toArray()返回的是一个Promise,因此这里需要再解析一层
return {db:result.db,blogs:blogs}
})
}).then(result=> {
console.log(result.blogs.length);
result.db.close();
}).catch(err=> {
console.log(err);
});
我们在每个then()的返回中,将db及每次的其他结果组成对象返回。请注意,若每次结果均为同步值则无妨;但若为Promise值,每个Promise都需额外解析一层。
例如上述示例中,第二个then()返回的 {db:result.db,blogs:result.coll.find().toArray()} 对象中,blogs是一个Promise。在下一个then()中,无法直接引用博客列表数组值,因此需先调用then()解析一层,再将两个同步值db和blogs返回。
注意,此处涉及Promise的嵌套,但一个Promise仅嵌套一层then()。
这种方式同样繁琐,因为若then()中返回的不是同步值而是Promise,需做大量额外工作。且每次透传“多余”的db对象,逻辑上略显冗余。
除上述情况外,若使用Promise链遇到此类问题,似乎暂无其他更好解决方案。只能根据场景选择“最优”方案。
鉴于Promise存在的上述问题,TJ大神利用ES6中的生成器函数,通过co模块包装,以更优雅方式解决上述问题。
co搭配生成器函数
若使用co模块搭配生成器函数,上述示例可改写如下:
const co = require('co');
co(function* (){
let db = yield MongoClient.connect(url + db_name);
let coll = db.collection('blogs');
let blogs = yield coll.find().toArray();
console.log(blogs.length);
db.close();
}).catch(err=> {
console.log(err);
});
co是一个函数,接收生成器函数作为参数并执行。生成器函数中使用yield关键字“同步”获取每个异步操作的值。
上述代码在形式上比Promise链更优雅,消除了回调函数,代码看起来均为同步操作(除了使用co和yield略显怪异)。
使用co模块需将所有操作包装为生成器函数,再使用co()调用。虽可接受,但ES的演进并未止步于此,async/await遂被提至ES7提案。
async/await
我们先看一下使用async/await改写后的代码:
(async function(){
let db = await MongoClient.connect(url + db_name);
let coll = db.collection('blogs');
let blogs = await coll.find().toArray();
console.log(blogs.length);
db.close();
})().catch(err=> {
console.log(err);
});
对比代码可见,async/await与co两种方式代码极为相似。
co换成了async,yield换成了await,同时生成器函数变为了普通函数。
此方式语义更清晰:async表明函数为异步,await表示需“等待”异步操作返回值。
async函数返回一个Promise,上述代码实际运作如下:
let getBlogs = async function(){
let db = await MongoClient.connect(url + db_name);
let coll = db.collection('blogs');
let blogs = await coll.find().toArray();
db.close();
return blogs;
};
getBlogs().then(result=> {
console.log(result.length);
}).catch(err=> {
console.log(err);
})
我们将getBlogs定义为async函数,最终返回的博客列表将被包装为Promise。如前所述,直接调用getBlogs().then()即可获取async函数返回值。
以上简要对比了三种解决异步的方案,下面深入了解一下async/await。
深入async/await
深入async/await
async返回值
async用于定义异步函数,该函数返回一个Promise。
若async函数返回同步值,该值将被包装为已resolve的Promise,等同于return Promise.resolve(value) 。
await用于异步操作之前,表示需“等待”该异步操作的返回值。await也可用于同步值。
//返回一个Promise
let timer = async functiontimer(){
return new Promise((resolve,reject) => {
setTimeout(()=> {
resolve('500');
},500);
});
}
timer().then(result=> {
console.log(result); //500
}).catch(err=> {
console.log(err.message);
});
//返回一个同步的值
let sayHi = async functionsayHi(){
let hi = await 'hello world';
return hi; //等同于return Promise.resolve(hi);
}
sayHi().then(result=> {
console.log(result);
});
上述示例返回同步值字符串'hello world',sayHi()为async函数,返回值被包装为Promise,可调用then()获取返回值。
对于同步值,可使用await,也可不使用,效果相同。具体是否使用,视情况而定。
例如前述MongoDB查询博客示例中,let coll = db.collection('blogs')未使用await,因为这是同步值。当然,也可使用await,使代码风格统一,尽管效果相同。
async函数的异常
let sayHi = async functionsayHi(){
throw new Error('出错了');
}
sayHi().then(result=> {
console.log(result);
}).catch(err=> {
console.log(err.message); //出错了
});
若在async函数中直接抛出异常,由于返回的是Promise,该异常可通过调用返回Promise的catch()方法捕获。
与Promise链的对比:
async函数中可包含多个异步操作,其异常处理与Promise链有相似之处:若有一个Promise被reject(),后续操作将不再执行。
let count = ()=>{
return new Promise((resolve,reject) => {
setTimeout(()=>{
reject('故意抛出错误');
},500);
});
}
let list = ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve([1,2,3]);
},500);
});
}
let getList = async ()=>{
let c = await count();
let l = await list();
return {count:c,list:l};
}
console.time('begin');
getList().then(result=> {
console.log(result);
}).catch(err=> {
console.timeEnd('begin');
console.log(err);
});
//begin: 507.490ms
//故意抛出错误
如上述代码,定义两个异步操作count和list,使用setTimeout延时500毫秒。count故意直接抛出异常,从输出结果看,count()抛出异常后,直接由catch()捕获,list()并未继续执行。
并行
并行
使用async后,上述示例均为串行执行。例如前述list()和count()示例,可将其用于分页查询数据场景。
先查询数据库总记录数,再根据分页条件查询分页数据,最后返回分页数据及分页信息。
前述示例中count()和list()有“先后顺序”,即先查总数,再查列表。实际上,这两个操作无先后关联,可异步同时执行,待所有结果返回后再拼装数据。
let count = ()=>{
return new Promise((resolve,reject) => {
setTimeout(()=>{
resolve(100);
},500);
});
}
let list = ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve([1,2,3]);
},500);
});
}
let getList = async ()=>{
let result = await Promise.all([count(),list()]);
return result;
}
console.time('begin');
getList().then(result=> {
console.timeEnd('begin'); //begin: 505.557ms
console.log(result); //[ 100, [ 1, 2, 3 ] ]
}).catch(err=> {
console.timeEnd('begin');
console.log(err);
});
我们将count()和list()使用Promise.all()“同时”执行,此处count()和list()可视为“并行”执行,耗时为两个异步操作中耗时最长者。
最终结果为两个操作结果组成的数组,只需按顺序取出数组中的值即可。
JavaScript中最棘手的问题莫过于回调函数嵌套。以往在浏览器中,与服务器通讯属昂贵操作,复杂业务逻辑多置于服务器端,前端JavaScript仅需少数几次AJAX请求即可获取全部数据。
然而,随着WebApp流行,前端业务逻辑日益复杂,多个AJAX请求间互有依赖:部分请求依赖前序请求数据,部分需并行进行。在Node.js等后端JavaScript环境中,因需大量IO操作,问题更为显著。此时使用回调函数组织代码往往导致代码难以阅读。
目前较流行的解决方案是使用Promise,可将嵌套回调函数展平。但编码与阅读仍存额外负担。
另一方案是使用ES6新增的generator,因generator本质可暂停函数执行并保存上下文,再次调用时恢复状态。co模块封装得当,但略显滥用generator特性之感。
ES7提供了更标准的解决方案,新增async/await两个关键词。async可声明异步函数,此函数需返回Promise对象。await可等待Promise对象resolve并获取结果。
如下示例,以往无法在JavaScript中使用常见sleep函数,只能使用setTimeout注册回调函数,指定时间后执行。有了async/await后,可实现如下方式:
async function sleep(timeout) {
return new Promise((resolve, reject) => {
setTimeout(function() {
resolve();
}, timeout);
});
}
(async function() {
console.log('Do some thing, ' + new Date());
await sleep(3000);
console.log('Do other things, ' + new Date());
})();
执行此段代码,可在终端中看到结果:
Do some thing, Mon Feb 23 2015 21:52:11 GMT+0800 (CST) Do other things, Mon Feb 23 2015 21:52:14 GMT+0800 (CST)
此外,async函数可正常返回结果和抛出异常。await函数调用即可获取结果,在外层包裹try/catch即可捕获异常。以下为从豆瓣API获取数据的示例:
var fetchDoubanApi = function() {
return new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
var response;
try {
response = JSON.parse(xhr.responseText);
} catch (e) {
reject(e);
}
if (response) {
resolve(response, xhr.status, xhr);
}
} else {
reject(xhr);
}
}
};
xhr.open('GET', 'https://api.douban.com/v2/user/aisk', true);
xhr.setRequestHeader("Content-Type", "text/plain");
xhr.send(data);
});
};
(async function() {
try {
let result = await fetchDoubanApi();
console.log(result);
} catch (e) {
console.log(e);
}
})();
async函数的用法
同Generator函数一样,async函数返回Promise对象,可使用then方法添加回调函数。函数执行时,一旦遇到await即先返回,待触发的异步操作完成,再接着执行函数体内后续语句。 下面是一个例子。
async function getStockPriceByName(name) {
var symbol = await getStockSymbol(name);
var stockPrice = await getStockPrice(symbol);
return stockPrice;
}
getStockPriceByName('goog').then(function (result){
console.log(result);
});
阅读本文前,期待您对promise和ES6(ECMA2015)有所了解,会更容易理解。本文以体验为主,不会深入说明,结尾有详细的文章引用。
第一个例子
Async/Await应为目前最简单的异步方案,首先来看个例子。此处要实现暂停功能,输入N毫秒,则停顿N毫秒后才继续往下执行。
var sleep = function (time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve();
}, time);
})
};
var start = async function () {
// 在这里使用起来就像同步代码那样直观
console.log('start');
await sleep(3000);
console.log('end');
};
start();
控制台先输出start,稍等3秒后,输出了end。
基本规则
async表示这是一个async函数,await只能用在此函数内部。await表示在此等待promise返回结果后,再继续执行。await后面跟着的应是一个promise对象(当然,其他返回值也没关系,只是会立即执行,不过那样就没有意义了…)
获得返回值
await等待的虽是promise对象,但不必写.then(..),直接可得返回值。
var sleep = function (time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
// 返回 ‘ok'
resolve('ok');
}, time);
})
};
var start = async function () {
let result = await sleep(3000);
console.log(result); // 收到 ‘ok'
};
捕捉错误
既然无需写.then(..),那么.catch(..)也不必写,可直接用标准try catch语法捕捉错误。
var sleep = function (time) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
// 模拟出错了,返回 ‘error'
reject('error');
}, time);
})
};
var start = async function () {
try {
console.log('start');
await sleep(3000); // 这里得到了一个返回错误
// 所以以下代码不会被执行了
console.log('end');
} catch (err) {
console.log(err); // 这里捕捉到错误 `error`
}
};
循环多个await
await看似同步代码,故可理所当然地写在for循环中,无需担心以往需闭包才能解决的问题。
..省略以上代码
var start = async function () {
for (var i = 1; i <= 10; i++) {
console.log(`当前是第${i}次等待..`);
await sleep(1000);
}
};
值得注意的是,await必须在async函数的上下文中。
..省略以上代码
let one2ten = [1,2,3,4,5,6,7,8,9,10];
// 错误示范
one2ten.forEach(function (v) {
console.log(`当前是第${v}次等待..`);
await sleep(1000); // 错误!! await只能在async函数中运行
});
// 正确示范
for(var v of one2ten) {
console.log(`当前是第${v}次等待..`);
await sleep(1000); // 正确, for循环的上下文还在async函数中
}
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
Java和JavaScript应用场景对比:企业级后端 vs Web前端与Node.js
2026-11-04 16:42:01
JavaScript模块化开发入门:CommonJS与ES Module的区别
2026-11-03 23:31:34
怎么创建并运行第一个js文件
2026-11-03 21:49:02
JavaScript运行环境怎么搭建?Node.js官方下载与安装教程
2026-11-03 20:03:04
NestJS 集成 TypeORM 或 Prisma 实现数据库持久化操作
2026-11-03 18:56:13
