解法
node.js 通过非阻塞 I/O 让高并发服务器变得容易,但代价体现在结构上:每次数据库查询或 API 调用都增加一层回调,而 koush 自己的项目——要处理成千上万的持久连接——经常嵌套七八层深。一个简单的请求加查询就已经需要两层回调,错误处理让每一层都有自己的 try/catch,而串行任务(比如分页 API 调用)变成了递归的续延函数。
2011 年可用的修复只是转移问题而不是消除问题:yield 运算符能挂起和恢复迭代器,但用起来笨拙;而 TameJS 这类源到源翻译器将干净的代码编译成普通 JavaScript,可在任何地方运行,但代价是调试生成的代码,而不是开发者写的代码。koush 的参照是 C# 5 的 async/await,它用显式的语言特性让异步代码读写起来如同同步代码。
他的做法是把这个特性构建进运行时:fork 一个 node.js,在 V8 中实现协程,加入 async 函数标记,并用 await 终止函数,在回调完成时恢复它。解析器生成的 AST 大致和回调版本相同,所以一个 try/catch 可以覆盖每个 await,一个 while 循环就能串行处理分页 API 请求,而 ndb 和 node-inspector 依然能够调试原始代码。代价是需要自定义 node 构建,但对于能控制自己服务器的团队来说,这个代价现实可行。
What it achieved
通过 V8 协程在 await 处挂起函数,使异步代码像同步代码一样书写
为什么有效
嵌套回调会随着每次 I/O 步骤成倍增加,并把错误处理推向各自的作用域;await 用一个 try/catch 围绕整个流程,让异常像同步代码一样穿越回调边界。
在运行时实现真正的协程,而不是源到源翻译,能让开发者编写的代码就是实际运行的代码,因此 ndb 和 node-inspector 调试的是原始代码,而非输出的代码。
顺序的控制流让普通循环能表达串行的异步工作:分页的 GDATA 示例用 while 循环替换了递归的 chunk 函数。
因为服务器团队控制自己的部署,fork node.js 和 V8 是一个现实可行的代价,而这个设计作为可工作的分支提供了,并带有测试,而不是一个提案。
可以借鉴什么
当变通方法将复杂性推入生成的代码或更深的嵌套时,要修复语言层面:在 I/O 处挂起整个函数,让可读的代码既被运行也被调试
后续
项目始终是分支,而未被合并:维基在 2013 年 4 月做了最后编辑,共 25 次修订,把自定义 node 构建定位为一种部署选项,其待办清单包含优化含有 await 的分支。在 2011 年 9 月 29 日发布到 Hacker News 时,获得 222 分和 101 条评论;读者认为这个做法非常优雅,并认同正确的修复是引入协程。记录中没有显示该补丁进入 node.js;它的可见效果是,用可工作的代码演示了 JavaScript 如何在语言层面获得 async/await。
从这里,追溯依据
参考来源
- "async" support in node.js (koush/node wiki) github.com
- Fixing the callback spaghetti in node.js | Hacker News news.ycombinator.com