我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情
- 本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
- 这是源码共读的第21期,链接juejin.cn/post/7083
序言
如今随着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的异常编程解决方案,只有了解历史背景,知道其来龙去脉,我们的知识体系才够完整。
- 错误优先的处理原则。
- 当我们遇到一些不合理的地方,或者明显痛点的时候,这个时候我们可以尝试多思考下,看有没有更优雅的解决方案,不论是从社区去找,还是自我思考,我觉得都会对自己有更好的成长。