本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

1. 前言

1.1 你能学到

  1. await-to-js出现的原因
  2. await-to-js使用以及原理

2. 准备

2.1 了解作用

以下一些代码片段来自官方博客,如果有中文注释,那就是我自己写的,如有错漏之处,敬请指正~

回调地狱

要问起 Promise 解决了什么,那第一个想到的必然是回调地狱,像这样的东西👇

  1. function AsyncTask() {
  2. asyncFuncA(function(err, resultA){
  3. if(err) return cb(err);
  4. asyncFuncB(function(err, resultB){
  5. if(err) return cb(err);
  6. asyncFuncC(function(err, resultC){
  7. if(err) return cb(err);
  8. // And so it goes....
  9. });
  10. });
  11. });
  12. }

有了 ES6Promise,上面的噩梦代码就可以简化为这样 👇

  1. function asyncTask(cb) {
  2. asyncFuncA.then(AsyncFuncB)
  3. .then(AsyncFuncC)
  4. .then(AsyncFuncD)
  5. .then(data => cb(null, data)
  6. .catch(err => cb(err));
  7. }

实际开发中的复杂异步流程

但可能你有这样的需求:

  • 在某一步结束后,你想要获取该步中的某个值来对其进行一些操作
  • 任何一步发生了错误都能及时且恰当地给到用户确切的错误

幸好,ES7 asyncawait的出现,让逻辑处理更为干净利落:

  1. async function asyncTask(cb) {
  2. const user = await UserModel.findById(1);
  3. if(!user) return cb('No user found');
  4. // 获取中间某一个的某个值,方便后面对其操作
  5. const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
  6. if(user.notificationsEnabled) {
  7. await NotificationService.sendNotification(user.id, 'Task Created');
  8. }
  9. if(savedTask.assignedUser.id !== user.id) {
  10. await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
  11. }
  12. cb(null, savedTask);
  13. }

但是,我错误处理呢?

处理错误时繁琐的代码

异步调用时,由于异步函数等待 Promise ,当 Promise 遇到错误,就会抛出一个异常,并在最后被 Promise 对象的 catch 方法给捕获 —— 而使用 async 和 await,就需要使用 try + catch 来捕获错误,这会导致这样:

  1. async function asyncTask(cb) {
  2. //每一处操作都需要一个 try+catch
  3. try {
  4. const user = await UserModel.findById(1);
  5. if(!user) return cb('No user found');
  6. } catch(e) {
  7. return cb('Unexpected error occurred');
  8. }
  9. try {
  10. const savedTask = await TaskModel({userId: user.id, name: 'Demo Task'});
  11. } catch(e) {
  12. return cb('Error occurred while saving task');
  13. }
  14. if(user.notificationsEnabled) {
  15. try {
  16. await NotificationService.sendNotification(user.id, 'Task Created');
  17. } catch(e) {
  18. return cb('Error while sending notification');
  19. }
  20. }
  21. if(savedTask.assignedUser.id !== user.id) {
  22. try {
  23. await NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you');
  24. } catch(e) {
  25. return cb('Error while sending notification');
  26. }
  27. }
  28. cb(null, savedTask);
  29. }

看起来,真不太好;而如果不用,出现了错误他只会默默地退出函数,这脱离了控制,是不可以接受的

或许有一个更为整洁的解决方案~

2.2 先用一下

从 README 中获取上手相关信息

安装

  1. npm i await-to-js --save

使用小demo

  1. import to from 'await-to-js'; //关键to方法
  2. // If you use CommonJS (i.e NodeJS environment), it should be:
  3. // const to = require('await-to-js').default;
  4. async function asyncTaskWithCb(cb) {
  5. let err, user, savedTask, notification;
  6. //从这里可以看出来,响应经过to函数,会解析为一个数组:[错误,数据]
  7. [ err, user ] = await to(UserModel.findById(1));
  8. if(!user) return cb('No user found'); //没有数据就退出并提示
  9. [ err, savedTask ] = await to(TaskModel({userId: user.id, name: 'Demo Task'}));
  10. if(err) return cb('Error occurred while saving task');//如果有错误就退出并提示
  11. //...为了篇幅,省略一些
  12. cb(null, savedTask);
  13. }
  14. async function asyncFunctionWithThrow() {
  15. const [err, user] = await to(UserModel.findById(1));
  16. if (!user) throw new Error('User not found'); //没有数据就抛出错误
  17. }

这形式,有点像 React 的 Hook 有没有 const [data,setData] = useState()

很明显,使用to方法后,代码变得更干净了,使其更为可读与可维护。这个方法就是该库的关键所在。

3 看看 源码

3.1 环境准备

这次其实环境都不用准备了,因为关键代码真的是非常地少,总共就22行。当然,如果你想顺便研究一下测试用例的话还是可以git clone一下的

3.2 理解源码

在看22行的关键代码之前,我们可以先看一个简版的实现(来自上面提到的官方博客)

  1. // to.js
  2. export default function to(promise) {
  3. return promise.then(data => {
  4. return [null, data]; //数组大小为2,第一项用null占位置,也达到了 !err == true 的效果
  5. })
  6. .catch(err => [err]); // 数组大小为1,根本没有第二项
  7. }

await是在等待解决的承诺,那么只要参数接收原先的 Promise再返回一个 处理过的 Promise—— 成功解析后变为一个数组,数据作为第二项,错误信息(如果有的话)作为第一项。
利用 Promise可以巧妙地达到该效果:

  • 成功:返回[null, data]
  • 失败:返回[err]

现在来看库中的代码,用了 TS ,如果你还没有学过 TS,那我建议你去学一下,不过这里没学的话也没关系,也就是约束以及告知开发者该处要用什么类型的数据罢了,我会告诉你这代码有什么作用

你也可以自己build一下,看打包后 JS 文件

  1. /**
  2. * @param { Promise } promise
  3. * @param { Object= } errorExt - Additional Information you can pass to the err object
  4. * @return { Promise }
  5. */
  6. export function to<T, U = Error> ( // T:Promise成功返回的数据类型,U:错误时返回类型,默认为Error
  7. promise: Promise<T>, //第一个参数只接受 Promise 对象
  8. errorExt?: object // 第二个参数是可选的,是一个错误信息的扩展
  9. ): Promise<[U, undefined] | [null, T]> { //返回的类型有两种:失败/成功
  10. return promise
  11. .then<[null, T]>((data: T) => [null, data]) //成功
  12. .catch<[U, undefined]>((err: U) => { //如果有错误
  13. if (errorExt) { //如果有错误信息的扩展
  14. const parsedError = Object.assign({}, err, errorExt); //就把他和错误信息一起合到一个空对象上
  15. return [parsedError, undefined];
  16. }
  17. return [err, undefined];
  18. });
  19. }
  20. export default to;

undefine & null

这里我一开始有点好奇的是,为什么失败的时候返回的是[err,undefined]而不是[err,null]呢?而成功的时候返回[null,data]而不是[undefined],null]呢?
要说最终要实现的效果,比如判定是否存在错误信息、判定有没有返回的数据,似乎也没有什么影响。

  1. //没有返回数据
  2. if(!undefined)console.log(1) //1
  3. //没有捕获到错误
  4. if(!null)console.log(1) //1
  5. !null === !undefined //true

看样子需要达到的效果相同,那么这里又是出于什么样的考虑来选用这两个数据类型呢?
或许与 typeof有关?

  1. typeof null //'object'
  2. typeof errorExt//'object'

为了这里的形式一致,所以在没有错误的时候让 null代替他的位置?
但这样为什么返回错误时就用 undefined 代表空值呢?


或许与这两个数据类型本身的意义有关?
null表示没有对象,即该处不应该有值。如果一切正常,错误信息自然是没有的——我们期望的就是一切正常,他本来就应该没有
undefined表示缺少值,就是此处应该有一个值,但是还没有定义。我们当然是期望可以得到响应的数据的,但此时出现了一些问题,导致本来应该有值的地方,丢失了值

以上都是个人推测,也没有找到什么依据,欢迎讨论~

3.3 测试样例

官方的测试代码:可以从中了解到使用to更多更具体的效果,以及学习到测试样例的书写分类:

  • 单纯一个 resolved 情况返回数据
  • rejected 情况返回错误
  • 有扩展错误信息的情况
    • 注意看错误时返回的类型设置
  • 开发者没有设置类型时,TS 自己推导类型够不够用

    4. 学习资源

  • await-to-js

  • 官方博客

    5. 总结 & 收获

  • 使代码更为简洁确实是令人愉悦

  • 接收一个 Promise再返回 处理过的Promise,最后以数组形式来存储错误/数据来方便判断,这方法真是巧妙啊