精读 Async/Await 更优越的 6 大理由

1,564 阅读6分钟
原文链接: zhuanlan.zhihu.com

本期精读的文章是:6 Reasons Why JavaScript’s Async/Await Blows Promises Away

1 引言

我为什么要选这篇文章呢?

前端异步问题处理一直是一个老大难的问题,前有 Callback Hell 的绝望,后有 Promise/Deferred 的规范混战,从 Generator 配合 co 所向披靡,到如今 Async/Await 改变世界。为什么异步问题如此难处理,Async/Await 又能在多大程度上解决我们开发和调试过程中遇到的难点呢?希望这篇文章能给我们带来一些启发。

当然,本文不是一篇针对前端异步问题综合概要性的文章,更多的是从 Async/Await 的优越性谈起。但这并不妨碍我们从 Async/Await 的特点出发,结合自己在工作、开发过程中的经验教训,认真的思考和总结如何更优雅、更高效的处理异步问题。

2 内容概要

Async/Await 的优点:

  • 语法简洁清晰,节省了很多不必要的匿名函数
  • 直接使用 try...catch... 进行异常处理
  • 添加条件判断更符合直觉
  • 减少不必要的中间变量
  • 更清晰明确的错误堆栈
  • 调试时可以轻松给每个异步调用加断点

Async/Await 的局限:

  • 降低了我们阅读理解代码的速度,此前看到 .then() 就知道是异步,现在需要识别 async 和 await 关键字
  • 目前支持 Async/Await 的 Node.js 版本(Node 7)并非 LTS 版本,但是下一个 LTS 版本很快将会发布

可以看出,文中提到 Async/Await 的优势大部分都是从开发调试效率提升层面来讲的,提到的问题或者说局限也只有不痛不痒的两点。

让我们来看看参与精读的同学都提出了哪些深度观点:

3 精读

本次提出独到观点的同学有:@javie007 @流形 @camsong @Turbe Xue @淡苍 @留影 @黄子毅 精读由此归纳。

Async/Await 并不是什么新鲜概念

参与精读的很多同学都提出来,Async/Await 并不是什么新鲜的概念,事实的确如此。

早在 2012 年微软的 C# 语言发布 5.0 版本时,就正式推出了 Async/Await 的概念,随后在 Python 和 Scala 中也相继出现了 Async/Await 的身影。再之后,才是我们今天讨论的主角,ES 2016 中正式提出了 Async/Await 规范。

以下是一个在 C# 中使用 Async/Await 的示例代码:

public async Task<int> SumPageSizesAsync(IList<Uri> uris) 
{
    int total = 0;
    foreach (var uri in uris) {
        statusText.Text = string.Format("Found {0} bytes ...", total);
        var data = await new WebClient().DownloadDataTaskAsync(uri);
        total += data.Length;
    }
    statusText.Text = string.Format("Found {0} bytes total", total);
    return total;
}

再看看在 JavaScript 中的使用方法:


async function createNewDoc() {
  let response = await db.post({}); // post a new doc
  return await db.get(response.id); // find by id
}

不难看出两者单纯在异步语法上,并没有太多的差异。这也是为什么 Async/Await 推出后,获得不少赞许和亲切感的原因之一吧。

其实在前端领域,也有不少类 Async/Await 的实现,其中不得不提到的就是知名网红之一的老赵写的 wind.js,站在今天的角度看,windjs 的设计和实现不可谓不超前。

Async/Await 是如何实现的

根据 Async/Await 的规范 中的描述 —— 一个 Async 函数总是会返回一个 Promise —— 不难看出 Async/Await 和 Promise 存在千丝万缕的联系。这也是为什么不少参与精读的同学都说,Async/Await 不过是一个语法糖。

单谈规范太枯燥,我们还是看看实际的代码。下面是一个最基础的 Async/Await 例子:


async function test() {
  const img = await fetch('tiger.jpg');
}

使用 Babel 转换后:


'use strict';

var test = function() {
    var _ref = _asyncToGenerator(regeneratorRuntime.mark(function _callee() {
        var img;
        return regeneratorRuntime.wrap(function _callee$(_context) {
            while (1) {
                switch (_context.prev = _context.next) {
                    case 0:
                        _context.next = 2;
                        return fetch('tiger.jpg');

                    case 2:
                        img = _context.sent;

                    case 3:
                    case 'end':
                        return _context.stop();
                }
            }
        }, _callee, this);
    }));

    return function test() {
        return _ref.apply(this, arguments);
    };
}();

function _asyncToGenerator(fn) {
    return function() {
        var gen = fn.apply(this, arguments);
        return new Promise(function(resolve, reject) {
            function step(key, arg) {
                try {
                    var info = gen[key](arg);
                    var value = info.value;
                } catch (error) {
                    reject(error);
                    return;
                }
                if (info.done) {
                    resolve(value);
                } else {
                    return Promise.resolve(value).then(function(value) {
                        step("next", value);
                    }, function(err) {
                        step("throw", err);
                    });
                }
            }
            return step("next");
        });
    };
}

不难看出,Async/Await 的实现被转换成了基于 Promise 的调用。值得注意的是,原来只需 3 行代码即可解决的问题,居然被转换成了 52 行代码,这还是基于执行环境中已经存在 regenerator 的前提之一。如果要在兼容性尚不是非常理想的 Web 环境下使用,代码 overhead 的成本不得不纳入考虑。

Async/Await 真的是更优秀的替代方案吗

不知道是个人观察偏差,还是大家普遍都有这样的看法。在国内前端圈子里,并没有对 Async/Await 的出现表现出多么大的兴趣,几种常见的观点是:「还不是基于 Promise 的语法糖,没什么意思」、「现在使用 co 已经能完美解决异步问题,不需要再引入什么新的概念」、「浏览器兼容性这么差,用 Babel 编译又需要引入不少依赖,使用成本太高」等等。

在本次精读中,也有不少同学指出了使用 Async/Await 的局限性。

比如,使用 Async/Await 并不能很好的支持异步并发。考虑下面这种情况,一个模块需要发送 3 个请求并在获得结果后才能进行渲染,3 个请求之间没有依赖关系。如果使用 Async/Await,写法如下:


async function mount() {
  const result1 = await fetch('a.json');
  const result2 = await fetch('b.json');
  const result3 = await fetch('c.json');

  render(result1, result2, result3);
}

这样的写法在异步上确实简洁不少,但是 3 个异步请求是顺序执行的,并没有充分利用到异步的优势。要想实现真正的异步,还是需要依赖 Promise.all 封装一层:


async function mount() {
  const result = await Promise.all(
    fetch('a.json'),
    fetch('b.json'),
    fetch('c.json')
  );

  render(...result);
}

此外,正如在上文中提到的,async 函数默认会返回一个 Promise,这也意味着 Promise 中存在的问题 async 函数也会遇到,那就是 —— 默认会静默的吞掉异常。

所以,虽然 Async/Await 能够使用 try...catch... 这种符合同步习惯的方式进行异常捕获,你依然不得不手动给每个 await 调用添加 try...catch... 语句,否则,async 函数返回的只是一个 reject 掉的 Promise 而已。

异步还有哪些问题需要解决

虽然处理异步问题的技术一直在进步,但是在实际工程实践中,我们对异步操作的需求也在不断扩展加深,这也是为什么各种 flow control 的库一直兴盛不衰的原因之一。

在本次精读中,大家肯定了 Async/Await 在处理异步问题的优越性,但也提到了其在异步问题处理上的一些不足:

  • 缺少复杂的控制流程,如 always、progress、pause、resume 等
  • 缺少中断的方法,无法 abort

当然,站在 EMCA 规范的角度来看,有些需求可能比较少见,但是如果纳入规范中,也可以减少前端程序员在挑选异步流程控制库时的纠结了。

3 总结

Async/Await 的确是更优越的异步处理方案,但我们相信这一定不是终极处理方案。随着前端工程化的深入,一定有更多、更复杂、更精细的异步问题出现,同时也会有迎合这些问题的解决方案出现,比如精读中很多同学提到的 RxJS 和 js-csp。

讨论地址是:那些年我们处理过的异步问题 · Issue #6 · dt-fe/weekly


如果你想参与讨论,请点击这里,每周都有新的主题,每周五发布。