源码学习——await-to-js:不用try-catch的异步等待

265 阅读6分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情

序言

如今随着IE浏览器的退出历史,现代的浏览器占据主流,我们可以应用越来越多的JS新特性,来提升我们的开发效率与开发体验,本篇文章我们来学习下那些异步编程的解决方案,以及遇到的问题。

背景知识

callback 回调函数

回调函数是早期解决异步编程的方式,主要用于setTimeout和ajax等异步方法中。

回调函数就是将函数callback作为参数传进函数,在适当的时机被调用执行。

setTimeout(function() {
        console.log('执行')
}, 1000);
// 伪代码 ajax请求
ajax(url, function(res) {
    console.log(res);
})

但这种方式有个非常恶心的缺点,就是容易出现回调地狱,也就是多个回调函数嵌套使用,使代码的可读性与可维护性变得非常差,并且不支持try-catch,无法在外层完成异常捕捉。只能在回调中处理异常。而且不支持return

setTimeout(function() {
    console.log('执行1')
        setTimeout(function() {
            console.log('执行2')
            setTimeout(function(){
                console.log('执行3')
            }, 3000);
    }, 2000)
}, 1000)

为了解决回调地狱,后来提供了Promise方案。

Promise

Promise实现了链式调用,每次then后会返回一个全新的Promise。如果在then中return返回值,则return的数据会被Promise.resolve()自动包装。

// 伪代码
ajax('http://xxx1')
    .then(function() {
        return ajax('http://xxx2');
    })
    .then(function(res) {
        console.log('操作');
    })
    .catch(function(err) {
        console.log('异常处理')
    })

虽然一定程度上解决了回调地狱,写法上也更优雅了。但还是会存在一些问题。最主要的就是如果不设置回调函数,promise内部抛出的错误,不会反应给外部。另外一个就是存在多个异步依赖调用的时候,后面的执行依赖前面的结果,这个时候一个是不断往下传递,或设置全局变量来接受,这样可能就比较复杂。

Generators/yield

这种方法在实际开发中很少使用,这里做下简单的介绍。 Generatore是ES6提供的异步解决方案,其可以控制函数的执行。可以理解成一个内部封装了很多状态的状态机,也是一个遍历器对象生成函数。其特征:

  • function关键字与函数名之间有一个星号。
  • 函数体内使用yield表达式,定义不同的内部状态。
    • 通过yield暂停函数,next启动函数,每次返回的是yield表达式结果。next可以接受参数,从而实现在函数运行的不同阶段。
    • 可以从外部往内部注入不同的值。next返回一个包含value和done的对象,其中value表达迭代的值,后者表达迭代是否完成。
function* iterationFun(x) {
            let y = yield(x+2);
            let z = 4*(yield(y/2));
            return (x+y+z);
        }
        let iterator = iterationFun(4); // 这个时候函数会被调用,但内部逻辑未执行,会返回一个iterator。
        console.log(iterator.next()); // 执行第一个yield 表达式 {value: 6, done: false}
        console.log(iterator.next(12)); // {value: 6, done: false}
        console.log(iterator.next(4)); // {value: 32, done: true}
  • 调用iterationFun不传参数时,next返回的value为NaN。
  • 调用iterationFun传入参数时,会作为上一个yield的值,所以第一次调用next时不用传参。
  • 第一次执行next时,函数会暂停在yield(x+2),返回的是4+2=5。
  • 第二次执行next时,传入的12会作为上一次yield表达式的值,也就是会将y赋值为12,返回的就是12/2=6
  • 第三次执行next时,传入的4作为上一次yeild表达式的值,也就是yield(y/2)这个时候会重新赋值为4。然后这个时候会执行所有的同步代码 z = 4*4=16 y=12 x = 4 最后返回16+12+4 = 32

async/await

async/await是ES7提出的,作为异步处理的终极解决方案 async本质是Gnerator函数的语法糖,对Generator做了改进:

  • 内置执行器:Generator函数的执行必须靠执行器也就是next,而async函数自带执行器,调用方式与普通函数一样。
  • 更好理解的语义:async表示定义异步函数,而await表示后面的表达式需要等待,相对于*和yield更语义化。
  • 适用性更广泛:co模块约定,yield命令后面只能是Thunk函数或Promise对象。而async函数的await命令后面可以是Promise或原始类型的值。
  • 返回Promise:async函数返回值是一个Promise对象,比Generator函数返回的Iterator对象更方便,可以直接使用then()方法进行链式调用。

语法如下:

 function delay(duraion) {
   return new Promise((reslove, reject) => {
        return setTimeout(reslove, duraion)
    })
}
async function run() {
    await delay(5000);
    return 'done';
}
run().then(res => {
    console.log(res);
})

但是async函数的错误处理,需要借助try catch来进行捕获,这样就额外增加了很多代码,有什么更好的方式来避免使用try catch块呢,接下来我们就来学习下await-to-js这个库,其优雅的解决了async函数的错误处理,利用了错误优先的处理原则。

源码解读

我们直接来看js的源码

function to(promise, errorExt) {
    return promise
        .then(function (data) { return [null, data]; })
        .catch(function (err) {
        if (errorExt) {
            var parsedError = Object.assign({}, err, errorExt);
            return [parsedError, undefined];
        }
        return [err, undefined];
    });
}

如何使用,这里直接使用源码库的案例

async function asyncTask(userId, cb) {
  let err, user, savedTask, notification;
  [ err, user ] = await to(UserModel.findById(userId));
  if(!(user && user.id)) return cb('No user found');
}

源码分析

原理其实是一种错误优先的处理原则。接受一个promise,然后将结果解析为一个数组,第一项存入promise的异常错误,第二项存入返回的数据。

  • 首先看传入参数,第一个是个promise对象,第二个是可选的错误信息。
  • to方法会直接返回传入的promise的执行。
  • then中法返回[null, data]null表示没有错误发生,data是最终的数据。
  • catch中,先判断是否传入了额外的错误对象,存在的话,利用Object.assign将传入的错误对象与promise返回的异常进行合并,然后返回[parsedError, undefined];不传入额外的错误对象,则直接返回promise的异常[err, undefined]

接下来我们用测试用来来调试下代码。

测试用例

利用测试用例去调试代码 源码中查看test/await-to-js.test文件,这里面都是一些测试用例。

命令行执行yarn test,就可以直接跑测试case。

我们可以看下都是从哪几个方面来保证代码的健壮性的。这里我们只看要测试的点即可,源码可以查看github.com/scopsy/awai…

  • 当成功时,返回一个正确的值。
  • 当失败时,返回一个异常。
  • 当传入额外异常对象时,返回的error对象应该存在传入的异常信息。

收获

  • 回顾复习了js的异常编程解决方案,只有了解历史背景,知道其来龙去脉,我们的知识体系才够完整。
  • 错误优先的处理原则。
  • 当我们遇到一些不合理的地方,或者明显痛点的时候,这个时候我们可以尝试多思考下,看有没有更优雅的解决方案,不论是从社区去找,还是自我思考,我觉得都会对自己有更好的成长。