假设现在是2016年,需求是:先获取用户信息(得到用户id),然后根据用户id获取用户的所有待办。 我们如何优雅的写代码?
注:Promise、Generator是ES2015的特性,async/await是ES2017的特性
Promise链式调用
我们可以通过Promise链式调用来控制函数的执行时机
function getUserInfo() {
// cookie session
return new Promise((resolve) => {
setTimeout(resolve({
id: 110,
name: '李向维',
}), 1000);
})
}
function getTodoList(userId) {
return new Promise((resolve) => {
setTimeout(resolve({
userId,
list: [{description: 'async语法糖了解'}, {description: '代码实践'}]
}), 1000);
})
}
// 任务执行
function initData() {
let todoList = [];
getUserInfo()
.then((user) => getTodoList(user.id))
.then((data) => {
todoList = data.list;
console.log('promise init data: ', JSON.stringify(todoList));
});
}
initData();
这种方式的缺点是,如果需要顺序执行的任务太多了,链式调用也会变长,开发体验开始下降。当然这还是比回调地狱强不少。
使用async/await
时间来到了2018年,ES2017发布了async/await这个特性,使得异步函数的调用代码也能写的和同步函数差不多。那我们来了解一些它吧!
语法介绍
-
定义
async 函数是使用
async关键字声明的函数。 async 函数是AsyncFunction构造函数的实例, 并且其中允许使用await关键字。内容来自MDN,更多关于async的语法描述请查看async 函数
-
返回值
aysnc函数返回一个Promise
使用
async function initData() {
const user = await getUserInfo();
const todoList = await getTodoList(user.id);
console.log('async init data: ', JSON.stringify(todoList));
}
代码组织相比上面的Promise链式调用明显更有优势。需要控制调用顺序的异步任务越多,优势越明显。
低版本浏览器支持
这里的低版本是相对不能支持async语法而言的
看这浏览器支持的情况,大部分系统使用async/await时应该都需要编译。
编译工具是如何用现有的语法实现此语法?babel在线编译
想要看懂编译结果,需要先了解JS Generator
因为编译结果中包含ES5支持Generator函数/对象的部分,代码有些多,为了突出async/await的支持,下面代码中先认为环境已经支持Generator函数。
-
使用generator function 组织异步调用的代码
这代码的组织形式就很简洁,类似async function,只是函数声明和关键字不同function* genInitData() { const user = yield getUserInfo(); const todoList = yield getTodoList(user.id); console.log('generator init data: ', JSON.stringify(todoList)); } -
执行这个generator function
const g = genInitData(); // 1. 执行第一个yield后暂停,getUserInfo() const userResult = g.next(); // 返回值的value属性一个promise,即为getUserInfo()的返回值 userResult.value.then((user) => { // 2. 执行到getTodoList,需要参数user.id,value属性值同样是promise // next的参数会赋值给yield左侧的变量,这是generator函数的语法 const todoResult = g.next(user.id); todoResult.value.then((todoList) => { // 3. 最后一次执行next,执行的是console.log语句 const {done} = g.next(todoList); // 4. 为true,标志generator函数执行完毕 console.log(done); }); });啊 这????
generator函数写起来倒是简单了,但这个执行过程未免太麻烦了,还不如promise链式调用。别着急,考虑到generator function的执行实际是有很明确的规则的,下面我们来把执行过程包装一下,让任意的generation function都能方便的执行。
-
generator function执行包装
// 1. 声明一个普通函数,接受参数generator function,返回值为promise function _asyncToGenerator(generatorFn) { return new Promise((resolve, reject) => { const generator = generatorFn() ; function _next(param) { asyncGeneratorStep(generator, 'next', param, resolve, reject, _next, _throw); } // 考虑错误捕获 function _throw(err) { asyncGeneratorStep(generator, 'throw', err, resolve, reject, _next, _throw); } // 第一次执行generator.next,没有参数 _next(); }); } // 2. 提供给_asyncToGenerator使用 function asyncGeneratorStep(generator, method, arg, resolve, reject, _next, _throw) { let result; try{ // generator.next or generator.throw调用 result = generator[method](arg); }catch(error) { reject(error); return; } if(result.done) { resolve(result.value); }else{ // Promise.resolve: yield执行后的返回值可能不是promise,包装一下,统一行为 Promise.resolve(result.value).then(_next, _throw); } } -
使用_asyncToGenerator执行之前的genInitData
有了_asyncToGenerator函数后,执行generator function就很简单了_asyncToGenerator(genInitData).then(() => { console.log('initData执行完成'); });
看到这里,总于明白为什么说async/await的是Promise和Generator的语法糖了,因为async/await是通过了巧妙的方式使用了Promise和Generator,并未拓展新功能。