使用Promise/Generators/Coroutines写现代异步Javascript

本文由原作者William Gottschalk原著于https://medium.freecodecamp.com/write-modern-asynchronous-javascript-using-promises-generators-and-coroutines-5fa9fe62cf74,文章原标题:《Write Modern Asynchronous Javascript using Promises, Generators, and Coroutines》

近几年,“回调地狱(Callback Hell)”一词经常被提及,成为Javascript并发管理中最为讨厌的设计之一。它让你忘记了代码本来应有的样子,以下便是Express中验证和处理一个交易的例子:

  1. app.post("/purchase", (req, res) => {
  2. user.findOne(req.body, (err, userData) => {
  3. if (err) return handleError(err);
  4. permissions.findAll(userData, (err2, permissions) => {
  5. if (err2) return handleError(err2);
  6. if (isAllowed(permissions)) {
  7. transaction.process(userData, (err3, confirmNum) => {
  8. if (err3) return handleError(err3);
  9. res.send("Your purchase was successful!");
  10. });
  11. }
  12. });
  13. });
  14. });

Promise应该可以拯救我们

Promise允许Javascript开发者像书写同步的代码一般书写异步代码,我们只需要把异步函数包裹在一个特殊的对象里即可。如果要访问Promise对象的值的话,只需要通过Promise对象的.then或者.catch方法即可获取。但当我们尝试通过Promise来重构上面的代码会发生什么呢?

  1. // 所有的异步方法已经被promise化了
  2. app.post("/purchase", (req, res) => {
  3. user.findOneAsync(req.body)
  4. .then( userData => permissions.findAllAsync(userData) )
  5. .then( permissions => {
  6. if (isAllowed(permissions)) {
  7. return transaction.processAsync(userData);
  8. // userData是undefined,这不在相应的作用域中
  9. }
  10. })
  11. .then( confirmNum => res.send("Your purchase was successful!") )
  12. .catch( err => handleError(err) )
  13. });

这样每一个回调函数属于一个单独的作用域,我们便不能在第二个.then回调函数里面访问user对象了。

在一阵思考过后,我仍然无法找到一个优雅的解决办法,只是找到了一个令人沮丧的办法:

只需要把你的Promise对象缩进,让他们有合适的作用域即可

把Promise对象缩进!?这不就又回到了原本锥型的样子了吗?

  1. app.post("/purchase", (req, res) => {
  2. user.findOneAsync(req.body)
  3. .then( userData => {
  4. return permissions
  5. .findAllAsync(userData)
  6. .then( permissions => {
  7. if (isAllowed(permissions)) {
  8. return transaction.processAsync(userData);
  9. }
  10. });
  11. })
  12. .then( confirmNum => res.send("Your purchase was successful!"))
  13. .catch( err => handleError(err) )
  14. });

我还计较原本那个嵌套的回调函数版本比这个嵌套的Promise版本看起来更清晰易懂呢。

Async/Await会拯救我们的

asyncawait关键字可以让我们当做写同步代码一样写Javascript代码。以下便是使用ES7语法写成的代码:

  1. app.post("/purchase", async function (req, res) {
  2. const userData = await user.findOneAsync(req.body);
  3. const permissions = await permissions.findAllAsync(userData);
  4. if (isAllowed(permissions)) {
  5. const confirmNum = await transaction.processAsync(userData);
  6. res.send("Your purchase was successful!")
  7. }
  8. });

不幸的是,包括async/await在内的大部分ES7的功能特性依旧没有被实现,因此,需要使用别的编译器来完成。但是,你能够使用ES6的特性来写十分类似于以上风格的代码,这已经被大多数现代浏览器和Node(v4.0+)实现了。

Generators和Coroutine组合

generator(生成器函数)是一个很棒的元编程工具。它能用来进行惰性求值、遍历内存密集型数据集合以及从多个使用如RxJs库的数据源中按需处理数据。

但是,我们并不想在产品代码中只使用generator,因为它让我们不得不去推理执行的顺序。并且每次我们调用下一个函数的时候,都会像goto语句一样跳回到generator中。

coroutine知道这一点,它通过包裹generator解决了这个问题,并且通过抽象避免了复杂性。

使用Coroutine的ES6版本

coroutine允许我们一次yield一个异步函数,让代码看起来是同步的。

请注意我使用的co库,co的Coroutine会立即执行generator,但是Bluebird的Coroutine会返回一个函数,你必须调用这个函数来执行generator。

  1. import co from 'co';
  2. app.post("/purchase", (req, res) => {
  3. co(function* () {
  4. const person = yield user.findOneAsync(req.body);
  5. const permissions = yield permissions.findAllAsync(person);
  6. if (isAllowed(permissions)) {
  7. const confirmNum = yield transaction.processAsync(user);
  8. res.send("Your transaction was successful!")
  9. }
  10. }).catch(err => handleError(err))
  11. // 如果在generator中的任意一步出现错误,coroutine会停止并且返回一个被reject的Promise对象
  12. });

让我们来列举一些使用coroutine的基本原则:

  1. 任意在yield右侧的函数必须返回一个Promise对象。
  2. 如果你想立刻执行你的代码,请使用co
  3. 如果你想稍后再执行你的代码,请使用co.warp
  4. 保证在你coroutine的尾部调用了.catch去捕获处理错误。否则,你就应该把你的代码包裹在一个try/catch块之中。
  5. BluebirdPromise.coroutine等价于Co的co.wrap,但并不等同于co自己的函数。

那该如何并发运行多条语句呢?

你可以使用对象或者数组,带上yield关键字,之后就可以通过解构来获取结果。

  1. import co from 'co';
  2. // 使用对象
  3. co(function*() {
  4. const {user1, user2, user3} = yield {
  5. user1: user.findOneAsync({name: "Will"}),
  6. user2: user.findOneAsync({name: "Adam"}),
  7. user3: user.findOneAsync({name: "Ben"})
  8. };
  9. ).catch(err => handleError(err))
  10. // 使用数组
  11. co(function*() {
  12. const [user1, user2, user3] = yield [
  13. user.findOneAsync({name: "Will"}),
  14. user.findOneAsync({name: "Adam"}),
  15. user.findOneAsync({name: "Ben"})
  16. ];
  17. ).catch(err => handleError(err))
  1. // 使用Bluebird库
  2. import {props, all, coroutine} from 'bluebird';
  3. // 使用对象
  4. coroutine(function*() {
  5. const {user1, user2, user3} = yield props({
  6. user1: user.findOneAsync({name: "Will"}),
  7. user2: user.findOneAsync({name: "Adam"}),
  8. user3: user.findOneAsync({name: "Ben"})
  9. });
  10. )().catch(err => handleError(err))
  11. // 使用数组
  12. coroutine(function*() {
  13. const [user1, user2, user3] = yield all([
  14. user.findOneAsync({name: "Will"}),
  15. user.findOneAsync({name: "Adam"}),
  16. user.findOneAsync({name: "Ben"})
  17. ]);
  18. )().catch(err => handleError(err))

当前你能用到的库: