深入理解ES7 async/await用法:从Promise到异步编程演进

作者:服务小站时间:2026-10-22 07:59:50浏览:586

本文深入解析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函数中
}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

作者最新文章