async/await 了解

181 阅读3分钟

假设现在是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这个特性,使得异步函数的调用代码也能写的和同步函数差不多。那我们来了解一些它吧!

语法介绍

  1. 定义

    async 函数是使用async关键字声明的函数。 async 函数是AsyncFunction构造函数的实例, 并且其中允许使用await关键字。

    内容来自MDN,更多关于async的语法描述请查看async 函数

  2. 返回值
    aysnc函数返回一个Promise Xnip2022-08-04_09-23-32.jpg

使用

async function initData() {
    const user = await getUserInfo();
    const todoList = await getTodoList(user.id);
    console.log('async init data: ', JSON.stringify(todoList));
}

代码组织相比上面的Promise链式调用明显更有优势。需要控制调用顺序的异步任务越多,优势越明显。

低版本浏览器支持

这里的低版本是相对不能支持async语法而言的 Xnip2022-08-04_09-42-07.jpg 看这浏览器支持的情况,大部分系统使用async/await时应该都需要编译。
编译工具是如何用现有的语法实现此语法?babel在线编译
想要看懂编译结果,需要先了解JS Generator

因为编译结果中包含ES5支持Generator函数/对象的部分,代码有些多,为了突出async/await的支持,下面代码中先认为环境已经支持Generator函数。

  1. 使用generator function 组织异步调用的代码
    这代码的组织形式就很简洁,类似async function,只是函数声明和关键字不同

    function* genInitData() {
        const user = yield getUserInfo();
        const todoList = yield getTodoList(user.id);
        console.log('generator init data: ', JSON.stringify(todoList));
    }
    
  2. 执行这个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);
        });
    });
    

    Xnip2022-08-04_21-04-17.jpg 啊 这????
    generator函数写起来倒是简单了,但这个执行过程未免太麻烦了,还不如promise链式调用。

    别着急,考虑到generator function的执行实际是有很明确的规则的,下面我们来把执行过程包装一下,让任意的generation function都能方便的执行。

  3. 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);
        }
    }
    
  4. 使用_asyncToGenerator执行之前的genInitData
    有了_asyncToGenerator函数后,执行generator function就很简单了

       _asyncToGenerator(genInitData).then(() => {
           console.log('initData执行完成');
       });
    

看到这里,总于明白为什么说async/await的是Promise和Generator的语法糖了,因为async/await是通过了巧妙的方式使用了Promise和Generator,并未拓展新功能。