本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
1. 学习任务和目标
- 任务发布: 【若川视野 x 源码共读】第21期 | await-to-js 如何优雅的捕获 await 的错误
- 读的库: github.com/scopsy/awai… 代码较少,简单易读,学完也能用上。
- 官方文章:How to write async await without try-catch blocks in Javascript
- 学习 await-to-js 使用和原理
2. 学习过程
3.1 初识 await-to-js 这个 npm 库
Async await wrapper for easy error handling without try-catch。
翻译过来就是:无需 try-catch 即可轻松处理错误的异步等待包装器。
简单地回顾一下 JavaScript 异步编程的进化之路。
- 第1阶段:早期的异步编程代码
在回调中嵌套着回调,形成了回调地狱,这使得维护代码和管理控制流非常困难:
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....
});
});
});
}
- 第2阶段:Promise 阶段
ES6 的 Promise 是一种优雅的异步编程解决方案,允许我们链式调用,解决了传统的回调地狱的问题。 异步编程可以写成如下格式:
function asyncTask(cb) {
asyncFuncA.then(AsyncFuncB)
.then(AsyncFuncC)
.then(AsyncFuncD)
.then(data => cb(null, data)
.catch(err => cb(err));
}
相比较于上面的回调地狱,使用 Promise 优雅了很多,可还是存在两个不足的地方
- 不够同步(代码依然会纵向延伸)
- 不能给每一次异步操作都进行错误处理 (这也就是为什么ES7中会出现 async/await,号称异步编程的最后解决方案的原因了)
- 第3阶段: ES7 Async/await
Note: You will need to use a transpiler in order to enjoy async/await, you can use either babel or typescript to the polyfills required.
async函数是Generator函数的语法糖。使用 关键字async来表示,在函数内部使用await来表示异步。相较于Generator,async函数的改进在于下面四点:
- 内置执行器。
Generator函数的执行必须依靠执行器,而async函数自带执行器,调用方式跟普通函数的调用一样- 更好的语义。
async和await相较于*和yield更加语义化- 更广的适用性。
co模块约定,yield命令后面只能是 Thunk 函数或 Promise对象。而async函数的await命令后面则可以是 Promise 或者 原始类型的值(Number,string,boolean,但这时等同于同步操作)- 返回值是 Promise。
async函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用then()方法进行调用
因此,代码可以被改写成这样:
async function asyncTask(cb) {
const asyncFuncARes = await asyncFuncA();
const asyncFuncBRes = await asyncFuncB(asyncFuncARes);
const asyncFuncCRes = await asyncFuncC(asyncFuncBRes);
}
可以对每一次异步操作进行错误处理
async function asyncTask(cb) {
try {
const asyncFuncARes = await asyncFuncA();
} catch (error) {
return new Error(error);
}
try {
const asyncFuncBRes = await asyncFuncB(asyncFuncARes);
} catch (error) {
return new Error(error);
}
try {
const asyncFuncCRes = await asyncFuncC(asyncFuncBRes);
} catch (error) {
return new Error(error);
}
}
虽然优化了 promise 的不足之处,但是每一次的异步操作都要用 try/catch 进行错误处理还是不够方便的。
- 第4阶段:使用 await-to-js 库,代码改写成:
import to from './to.js';
async function asyncTask() {
const [err1, asyncFuncARes] = await to(asyncFuncA());
if (err1) throw new Error(err1);
const [err2, asyncFuncBRes] = await to(asyncFuncB(asyncFuncARes));
if (err2) throw new Error(err2);
const [err3, asyncFuncCRes] = await to(asyncFuncC(asyncFuncBRes));
if (err3) throw new Error(err3);
}
3.2 await-to-js 如何使用和源码分析
安装依赖:npm i await-to-js --save
使用:
import to from 'await-to-js';
// 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;
[ 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');
if(user.notificationsEnabled) {
[ err ] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
if(err) return cb('Error while sending notification');
}
if(savedTask.assignedUser.id !== user.id) {
[ err, notification ] = await to(NotificationService.sendNotification(savedTask.assignedUser.id, 'Task was created for you'));
if(err) return cb('Error while sending notification');
}
cb(null, savedTask);
}
async function asyncFunctionWithThrow() {
const [err, user] = await to(UserModel.findById(1));
if (!user) throw new Error('User not found');
}
TS版本的源码:
export function to<T, U = Error> (
promise: Promise<T>,
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;
JS版本的源码:
export function to( promise, errorExt) {
return promise
.then((data) => [null, data]) // 成功,返回[null, 响应结果]
.catch((err) => {
if (errorExt) {
const parsedError = Object.assign({}, err, errorExt);
return [parsedError, undefined]; // 失败,返回[错误信息, undefined]
}
return [err, undefined];
});
}
export default to;
从代码中可以看到
- 无论成功还是失败,都返回一个数组;
- 成功的话,返回
[null, 响应结果]; - 失败的话,返回
[错误信息, undefined]; errorExt是用户自定义错误信息,通过Object.assign将正常返回的error和用户自定义的合并到一个对象里面供用户自己选择。
3. 简单总结
确实是一个简短而精美的库,可以在项目中尝试使用。 我个人习惯了promise 链式调用写法,也觉得蛮方便的。 是否有必要引入使用,就看你的需求场景吧。