解决方案
node.js 通过一切皆走非阻塞 I/O,让高并发服务器变得容易,但代价体现在结构上:每一次数据库查询或 API 调用都会增加一层回调,而 koush 自己的项目需要承载数万条长时间运行的连接,常常嵌套到七八层缩进。一个简单的请求加查询就已经需要两层嵌套回调,错误处理在每一层各自包一个 try/catch,而诸如分页 API 调用这类串行化工作则变成了递归的续延函数。
2011 年可用的补救办法只是把问题挪了个位置,而非消除它:yield 运算符能挂起和恢复迭代器,但用起来依然笨拙;而 TameJS 这类源到源翻译器则把看起来干净的代码编译成可在任何地方运行的普通 JavaScript,代价是调试生成后的代码,而不是开发者写下的代码。Koush 的参照点是 C# 5 的 async/await——一个显式的语言特性,让异步代码读起来像同步代码。
他的做法是把这一特性直接做进运行时:一个在 V8 中实现协程的 node.js 分支、一个 async 函数标记,以及一个 await——它会终止函数,并在回调完成时恢复该函数。解析器生成的 AST 仍与回调版本大致相同,因此一个 try/catch 就能覆盖所有 await,一个 while 循环就能串行化分页 API 请求,同时 ndb 和 node-inspector 仍能调试原始代码。代价是需要一个定制的 node 构建,而对于掌控自己服务器的团队来说,这个代价是可以现实地承担的。
取得了什么
通过 V8 协程在 await 处挂起函数,让异步代码读起来像同步代码
为何奏效
嵌套回调会随每一个 I/O 步骤成倍增加,并把错误处理推入各自独立的作用域;await 则让整个流程共用一个 try/catch,使异常像同步代码那样跨越回调边界。
运行时中的真正协程,而非源到源翻译,让开发者写下的代码就是实际运行的代码,因此 ndb 和 node-inspector 调试的是原始代码,而非生成输出。
看似顺序执行的控制流让普通循环就能表达串行化的异步工作:分页 GDATA 的例子用 while 循环取代了递归的分块函数。
由于服务器团队掌控自己的部署,分叉 node.js 和 V8 是一个现实的代价,而且该设计以带测试的可运行分支形式发布,而非一份提案。
可以借鉴什么
当变通办法把复杂性推入生成代码或更深的嵌套时,就在语言层面修复:在 I/O 处挂起整个函数,让可读的代码既是实际运行的代码,也是你调试的代码
后续
该项目始终是一个分支,而非被合并的特性:其 wiki 经过 25 次修订、最后编辑于 2013 年 4 月,把定制的 node 构建定位为一种部署选项,其 TODO 列出了对包含 await 的分支进行优化。该项目于 2011 年 9 月 29 日发布到 Hacker News,获得 222 分和 101 条评论;读者称这种方法非常优雅,并认同正确的修复方式是引入协程。记录中没有任何内容显示该补丁进入了 node.js;其可见的影响是用可运行的代码展示了 JavaScript 如何在语言层面获得 async/await。
追溯依據
參考來源
- "async" support in node.js (koush/node wiki) GitHub — koush/node wiki · github.com
- Fixing the callback spaghetti in node.js | Hacker News Hacker News · news.ycombinator.com ·