本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 前言
1.1 你能学到
await-to-js
出现的原因await-to-js
使用以及原理
2. 准备
2.1 了解作用
以下一些代码片段来自官方博客,如果有中文注释,那就是我自己写的,如有错漏之处,敬请指正~
回调地狱
要问起 Promise 解决了什么,那第一个想到的必然是回调地狱,像这样的东西👇
function AsyncTask() {
asyncFuncA(function(err, resultA){
if(err) return cb(err);
asyncFuncB(function(err, resultB){
if(err) return cb(err);
asyncFuncC(function(err, resultC){
if(err) return cb(err);
// And so it goes....
});
});
});
}
有了 ES6
和 Promise
,上面的噩梦代码就可以简化为这样 👇
function asyncTask(cb) {
asyncFuncA.then(AsyncFuncB)
.then(AsyncFuncC)
.then(AsyncFuncD)
.then(data => cb(null, data)
.catch(err => cb(err));
}
实际开发中的复杂异步流程
但可能你有这样的需求:
- 在某一步结束后,你想要获取该步中的某个值来对其进行一些操作
- 任何一步发生了错误都能及时且恰当地给到用户确切的错误
- …
幸好,ES7 async
、await
的出现,让逻辑处理更为干净利落:
async function asyncTask(cb) {
const user = await UserModel.findById(1);
if(!user) return cb('No user found');
// 获取中间某一个的某个值,方便后面对其操作
const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
if(user.notificationsEnabled) {
await NotificationService.sendNotification(user.id, 'Task Created');
}
if(savedTask.assignedUser.id !== user.id) {
await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
}
cb(null, savedTask);
}
处理错误时繁琐的代码
异步调用时,由于异步函数等待 Promise ,当 Promise 遇到错误,就会抛出一个异常,并在最后被 Promise 对象的 catch 方法给捕获 —— 而使用 async 和 await,就需要使用 try + catch 来捕获错误,这会导致这样:
async function asyncTask(cb) {
//每一处操作都需要一个 try+catch
try {
const user = await UserModel.findById(1);
if(!user) return cb('No user found');
} catch(e) {
return cb('Unexpected error occurred');
}
try {
const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
} catch(e) {
return cb('Error occurred while saving task');
}
if(user.notificationsEnabled) {
try {
await NotificationService.sendNotification(user.id, 'Task Created');
} catch(e) {
return cb('Error while sending notification');
}
}
if(savedTask.assignedUser.id !== user.id) {
try {
await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
} catch(e) {
return cb('Error while sending notification');
}
}
cb(null, savedTask);
}
看起来,真不太好;而如果不用,出现了错误他只会默默地退出函数,这脱离了控制,是不可以接受的
2.2 先用一下
安装
npm i await-to-js --save
使用小demo
import to from 'await-to-js'; //关键to方法
// If you use CommonJS (i.e NodeJS environment), it should be:
// const to = require('await-to-js').default;
async function asyncTaskWithCb(cb) {
let err, user, savedTask, notification;
//从这里可以看出来,响应经过to函数,会解析为一个数组:[错误,数据]
[ err, user ] = await to(UserModel.findById(1));
if(!user) return cb('No user found'); //没有数据就退出并提示
[ err, savedTask ] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
if(err) return cb('Error occurred while saving task');//如果有错误就退出并提示
//...为了篇幅,省略一些
cb(null, savedTask);
}
async function asyncFunctionWithThrow() {
const [err, user] = await to(UserModel.findById(1));
if (!user) throw new Error('User not found'); //没有数据就抛出错误
}
这形式,有点像 React 的 Hook 有没有
const [data,setData] = useState()
很明显,使用to
方法后,代码变得更干净了,使其更为可读与可维护。这个方法就是该库的关键所在。
3 看看 源码
3.1 环境准备
这次其实环境都不用准备了,因为关键代码真的是非常地少,总共就22行。当然,如果你想顺便研究一下测试用例的话还是可以git clone
一下的
3.2 理解源码
在看22行的关键代码之前,我们可以先看一个简版的实现(来自上面提到的官方博客)
// to.js
export default function to(promise) {
return promise.then(data => {
return [null, data]; //数组大小为2,第一项用null占位置,也达到了 !err == true 的效果
})
.catch(err => [err]); // 数组大小为1,根本没有第二项
}
await
是在等待解决的承诺,那么只要参数接收原先的 Promise
再返回一个 处理过的 Promise
—— 成功解析后变为一个数组,数据作为第二项,错误信息(如果有的话)作为第一项。
利用 Promise
可以巧妙地达到该效果:
- 成功:返回
[null, data]
- 失败:返回
[err]
现在来看库中的代码,用了 TS ,如果你还没有学过 TS,那我建议你去学一下,不过这里没学的话也没关系,也就是约束以及告知开发者该处要用什么类型的数据罢了,我会告诉你这代码有什么作用
你也可以自己
build
一下,看打包后 JS 文件
/**
* @param { Promise } promise
* @param { Object= } errorExt - Additional Information you can pass to the err object
* @return { Promise }
*/
export function to<T, U = Error> ( // T:Promise成功返回的数据类型,U:错误时返回类型,默认为Error
promise: Promise<T>, //第一个参数只接受 Promise 对象
errorExt?: object // 第二个参数是可选的,是一个错误信息的扩展
): Promise<[U, undefined] | [null, T]> { //返回的类型有两种:失败/成功
return promise
.then<[null, T]>((data: T) => [null, data]) //成功
.catch<[U, undefined]>((err: U) => { //如果有错误
if (errorExt) { //如果有错误信息的扩展
const parsedError = Object.assign({}, err, errorExt); //就把他和错误信息一起合到一个空对象上
return [parsedError, undefined];
}
return [err, undefined];
});
}
export default to;
undefine & null
这里我一开始有点好奇的是,为什么失败的时候返回的是[err,undefined]
而不是[err,null]
呢?而成功的时候返回[null,data]
而不是[undefined],null]
呢?
要说最终要实现的效果,比如判定是否存在错误信息、判定有没有返回的数据,似乎也没有什么影响。
//没有返回数据
if(!undefined)console.log(1) //1
//没有捕获到错误
if(!null)console.log(1) //1
!null === !undefined //true
看样子需要达到的效果相同,那么这里又是出于什么样的考虑来选用这两个数据类型呢?
或许与 typeof
有关?
typeof null //'object'
typeof errorExt//'object'
为了这里的形式一致,所以在没有错误的时候让 null
代替他的位置?
但这样为什么返回错误时就用 undefined
代表空值呢?
或许与这两个数据类型本身的意义有关?null
表示没有对象,即该处不应该有值。如果一切正常,错误信息自然是没有的——我们期望的就是一切正常,他本来就应该没有undefined
表示缺少值,就是此处应该有一个值,但是还没有定义。我们当然是期望可以得到响应的数据的,但此时出现了一些问题,导致本来应该有值的地方,丢失了值
以上都是个人推测,也没有找到什么依据,欢迎讨论~
3.3 测试样例
官方的测试代码:可以从中了解到使用to
更多更具体的效果,以及学习到测试样例的书写分类: