一个函数,为什么会有“颜色”?
Robert Nystrom 在 2015 年那篇经典博客《What Color is Your Function?》里,没有先讲事件循环、线程、Future、Promise,而是编了一个怪语言:函数只有两种,红色和蓝色。
蓝函数像同步函数,调用后直接拿值。红函数像异步函数,结果晚点回来,得靠 callback、Promise、Task/Future 之类的东西承载。麻烦不在颜色本身,而在传染:你调用了红函数,你的函数往往也得变红。
这就是那篇文章十年后还扎人的地方。它讲的不是某个语法糖顺不顺手,而是异步模型进入语言和核心库后,怎么长期改写 API、复用边界和程序员的脑内地图。
红蓝函数讲的是异步如何传染
Nystrom 的寓言很短,但刀口很准。
| 寓言规则 | 现实含义 | 直接后果 |
|---|---|---|
| 蓝函数 | 同步函数 | 调用后直接拿返回值 |
| 红函数 | 异步函数 | 结果稍后到来,要用 callback、Promise、Task/Future 包起来 |
| 红函数只能从红函数里自然调用 | 异步结果不能在同步调用栈里直接变成普通值 | 调用者被迫 async 化 |
| 核心库里有大量红函数 | I/O、网络、文件等 API 暴露异步差异 | 业务层跟着染色 |
callback hell 不是程序员不会缩进,也不是代码品味差。它是平台把异步差异暴露给所有调用者后的自然结果。
Node 早期回调风格最痛的地方,是组合差、错误处理别扭、控制流断裂。你本来只想读个文件、查个接口、写段业务逻辑,结果代码形状先被异步模型拧了一遍。
Promise 是进步。它把异步操作变成一等对象,能链式组合,也更适合库封装。可它没有把红蓝世界合并。你拿到的仍不是值,而是一个“将来可能有值”的容器。
async/await 又往前走了一步。它让异步代码看起来更像同步代码,异常处理、循环、表达式都顺手许多。
但颜色还在。你要写 await,要返回 Promise、Task 或 Future,上层函数也常常要继续 async。语法糖把疼痛压下去了,没有把病灶切掉。
真正受影响的是 API,而不是几行代码
我不太买账的一种说法是:async/await 已经解决异步问题。
它解决的是书写体验里最疼的部分,不是抽象边界的问题。一个底层函数从同步改成异步,通常不是内部换个实现那么简单。调用它的封装函数、工具函数、测试代码、框架扩展点,都要重新表态。
受影响最具体的,是两类人。
写业务的开发者,会发现“加一个异步调用”很少只改一行。函数签名变了,测试写法变了,错误处理位置变了,调用方也要跟着改。
写库和框架的人更难。API 一旦公开,红蓝边界就变成契约。你今天返回一个普通值,明天改成 Promise,就不是小修小补,而是让使用者迁移调用方式。
现实里能做的不是消灭异步,而是控制染色半径:
| 场景 | 更稳的做法 | 代价 |
|---|---|---|
| 底层 I/O 必然异步 | 把 async 边界固定在少数模块 | 边界层代码会更厚 |
| 对外暴露库 API | 尽早决定同步版、异步版,别频繁改签名 | 早期设计成本更高 |
| 测试异步逻辑 | 明确使用 async 测试、mock Promise/Future | 测试工具链要跟上 |
| 业务函数复用 | 避免让无关工具函数随便染红 | 有时要多写适配层 |
这对团队选型也有现实含义。
如果项目大量依赖网络、数据库、队列和文件 I/O,选择显式 async 模型并不奇怪,甚至更诚实。成本摆在台面上,调度和等待都能被看见。
如果项目主要是 CPU 计算、规则处理、同步流程编排,那就要谨慎。别为了“现代”把一整条调用链染红。模型看着更先进,维护面可能更大。
“名不正,则言不顺。”函数看起来都叫函数,但一个返回值,一个返回未来值的包装。名字一样,抽象已经开始分叉。
接下来要看的是颜色边界,而不是关键字
有些语言靠线程和阻塞模型,让调用者暂时感觉不到颜色。比如传统 Java 代码里,很多调用看起来仍是普通函数调用。代价并没有消失,它藏在线程、调度、资源占用里。
另一些语言和生态选择让异步显形。JavaScript 是最典型的例子。它把等待写进 Promise、await 和调用链里,开发者很早就能看到成本,也很早就会被成本缠住。
这两条路没有纯粹的胜利者。
隐藏颜色,调用体验更统一,但底层资源账更容易被忽略。显式颜色,系统行为更可见,但 API 会被分成两套。并发不是免费午餐,只是账单寄给谁不同。
所以接下来最该观察的,不是某个语言又加了什么 async 语法,而是两个变量。
一个是核心库怎么设计边界。文件、网络、数据库、定时器这些基础能力,如果全部把颜色外溢给调用者,业务层就会长期背债。
另一个是框架怎么隔离颜色。好的框架会把异步集中在生命周期、请求处理、任务调度这些位置,不让每个小工具函数都被迫站队。差的框架会把红色一路泼到最上层,最后连一个简单校验函数都要想返回值模型。
这也是 Nystrom 那篇文章耐读的原因。它没有把 callback、Promise、async/await 判死刑。它只是提醒开发者:语法可以润色,调用链上的债不会自动消失。
Promise 不是废物,async/await 也不是骗局。它们都是止痛药。止痛药很好,但不能假装已经做完手术。
颜色真正要命的地方,不在多写几个字符,而在它改变了库的边界、复用的半径和团队迁移的成本。红蓝函数这根刺,扎的不是语法审美,是 API 设计的骨头。
