本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
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+catchtry {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.jsexport 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:错误时返回类型,默认为Errorpromise: 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更多更具体的效果,以及学习到测试样例的书写分类:
