Intro
异步代码从最早的 callback hell,到现在的 ES7 我们已经可以用 async/await 来像编写同步代码一样优雅的编写了。
什么是回调地狱(callback hell)呢,看下面的代码就一目了然了:
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....
});
});
});
}
这样的代码可读性和维护性都非常差,后来社区又引入了 promise:
function asyncTask(cb) {
asyncFuncA.then(AsyncFuncB)
.then(AsyncFuncC)
.then(AsyncFuncD)
.then(data => cb(null, data)
.catch(err => cb(err));
}
这样看上去确实是干净多了,但实际的开发中写多了会发现非常复杂的异步情况时,大量的 then 写起来还是有点累赘,而且很多时候的流程控制并非像这样线性的考虑,可能会出现很多分支。
ES7 终于推出了 async/await 标准,虽然浏览器并不直接支持,但我们使用 babel transpiler 或者 typescript 都可以享受这一特性。
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);
}
同样的还有基于 Generator 的解决方案:
*function asyncTask() {
yield task1();
yield task2();
yield task3();
yield task4();
}
可以看到我们编写异步代码简直和同步代码几乎没有什么区别。代码看上去也干净利落,但有一个问题是,如果需要处理异步调用中的错误情况的话,只能使用 try catch,导致生产中我们的代码可能变成这样:
async function asyncTask(cb) {
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);
}
像 dva 这样的封装了 React/Redux-saga 的框架提供了一个捕获 effects 中错误的 onError hook,但使用一些其他框架时往往还是需要使用 try catch,老实说大量的重复编写这样的代码会感觉非常糟心。
Go-like pattern
在 Golang 中是没有 try catch 的,因为它通过返回多个值来解决这一问题,典型的 Go 代码是这样的:
data, err := db.Query("SELECT ...")
if err != nil { return err }
随着 ES6 中解构赋值这一特性的引入,很快就有想到了像 Go 这样返回一个 error 和 resolve 值组成的数组这样有趣的方案:
const [err, data] = await someTask();
if (err) return;
由于 await 出错时如果没有 try catch 的话会静默的退出当前函数执行,因此我们需要一个简单的转换工具函数:
// to.js
export default function to(promise) {
return promise.then(data => [null, data])
.catch(err => [err, null]);
}
接下来就可以愉快的改写前面的代码了:
import to from './to.js';
async function asyncTask(cb) {
let err, user, savedTask;
[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) {
const [err] = await to(NotificationService.sendNotification(user.id, 'Task Created'));
if(err) return cb('Error while sending notification');
}
cb(null, savedTask);
}
最后的代码看上去舒服多了有没有!当然上面这样的只是一个简单的工具函数,生产环境使用的话可能需要做更多的处理。
虽然平时的工作中还不能使用,但这样的 Go 风格代码我个人非常喜欢,感觉非常有意思。