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

译文2016-07-160 篇评论 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 中验证和处理一个交易的例子:

app.post("/purchase", (req, res) => {
    user.findOne(req.body, (err, userData) => {
        if (err) return handleError(err);
        permissions.findAll(userData, (err2, permissions) => {
            if (err2) return handleError(err2);
            if (isAllowed(permissions)) {
                transaction.process(userData, (err3, confirmNum) => {
                    if (err3) return handleError(err3);
                    res.send("Your purchase was successful!");
                });
            }
        });
    });
});

Promise 应该可以拯救我们

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

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

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

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

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

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

app.post("/purchase", (req, res) => {
    user.findOneAsync(req.body)
        .then( userData => {
            return permissions
                .findAllAsync(userData)
                .then( permissions => {
                    if (isAllowed(permissions)) {
                        return transaction.processAsync(userData);
                    }
            });
        })
        .then( confirmNum => res.send("Your purchase was successful!"))
        .catch( err => handleError(err) )
});

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

Async/Await 会拯救我们的

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

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

不幸的是,包括 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。

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

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

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

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

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

import co from 'co';
// 使用对象
co(function*() {
    const {user1, user2, user3} = yield {
        user1: user.findOneAsync({name: "Will"}),
        user2: user.findOneAsync({name: "Adam"}),
        user3: user.findOneAsync({name: "Ben"})
    };
).catch(err => handleError(err))

// 使用数组
co(function*() {
    const [user1, user2, user3] = yield [
        user.findOneAsync({name: "Will"}),
        user.findOneAsync({name: "Adam"}),
        user.findOneAsync({name: "Ben"})
    ];
).catch(err => handleError(err))

// 使用 Bluebird 库
import {props, all, coroutine} from 'bluebird';

// 使用对象
coroutine(function*() {
    const {user1, user2, user3} = yield props({
        user1: user.findOneAsync({name: "Will"}),
        user2: user.findOneAsync({name: "Adam"}),
        user3: user.findOneAsync({name: "Ben"})
    });
)().catch(err => handleError(err))

// 使用数组
coroutine(function*() {
    const [user1, user2, user3] = yield all([
        user.findOneAsync({name: "Will"}),
        user.findOneAsync({name: "Adam"}),
        user.findOneAsync({name: "Ben"})
    ]);
)().catch(err => handleError(err))

当前你能用到的库:

评论区

发表评论
用户名
(必填)
电子邮箱
(必填)
个人网站
(选填)
评论内容
Copyright © 2017 dremy.cn
皖ICP备16015002号