Hotdry.

Article

ClojureScript 原生 async/await 语法的工程现状与实现路径

梳理 ClojureScript 异步编程的语法糖现状:实验性编译器扩展的用法、Promesa 与 core-async 的权衡,以及语言级别支持的边界条件。

2026-05-08compilers

在 ClojureScript 项目中处理异步代码时,开发者经常面临一个选择题:是否需要原生 ES 风格的 async/await 语法糖?截至 2026 年上半年,ClojureScript 官方并未将 async/await 纳入语言层面的第一等支持,而是将异步抽象交给 core-async 与 Promise 封装库处理。这一现状背后涉及编译器实现、CLJS 代码生成特性以及生态系统兼容性的多重考量。

异步抽象的历史分歧

Clojure 系列语言在异步编程上一直倾向于使用通道(Channel)模型。core-async 库引入的 <!>! 符号提供了类似协程的阻塞式写法,编译后通过状态机实现非阻塞执行。这种设计在处理高并发 IO 密集型任务时表现出色,但也意味着与 JavaScript 原生 Promise 生态的互操作需要额外适配层。

与此同时,现代 JavaScript 生态已全面倒向 async/await 语法。无论是浏览器端调用 Fetch API,还是 Node.js 生态中的绝大多数异步库,默认接口均为返回 Promise 的 async 函数。当 ClojureScript 代码需要与这些第三方库交互时,频繁的 .then 链式调用或 Promise 手动包装会显著降低代码可读性。

实验性编译器扩展的尝试

社区开发者 Roman Lafantan 维护的 cljs-async-await 项目是目前最接近原生语法支持的实验方案。该扩展通过宏定义的方式在 ClojureScript 中实现了 async 块与 await 操作符,其核心机制如下:

(require '[async-await.core :refer [async await]])

(defn http-get [url]
  (async
    (let [response (await (js/fetch url))
          json     (await (.json response))]
      (.log js/console json))))

编译阶段,这段代码会被转换为 ES2017+ 的 async 函数。值得注意的是,使用该扩展需要将编译器的 :language-in 选项设置为 :ecmascript-2017 或更高版本。如果目标运行环境不支持 async/await,Google Closure Compiler 会自动将其下转译为基于生成器与 Promise 的兼容实现。

然而,该方案存在一个关键限制:ClojureScript 的代码生成逻辑会将 do 表达式翻译为自调用函数(self-invoking function),这导致在某些上下文中 await 可能出现在非 async 函数内部,从而引发编译错误。例如:

(async
  (let [x (do (inc 1) (await 2))]
    x))

上述代码在编译后会生成如下无效 JavaScript:

(async function() {
  var x = (function() {
    1 + 1;
    return await 2;
  })();
  return x;
})()

这种边界情况要求开发者在编写时避开复杂的嵌套表达式,或者手动重构代码结构。

库层面的替代方案

对于不愿引入编译器扩展的项目,Promesa 库提供了更为保守的路径。Promesa 本质上是 JavaScript Promise 的轻量包装,通过 p/thenp/catch 等函数提供组合式异步编程能力,并提供了类似 async/await 的 p/let 宏来模拟顺序执行效果:

(require '[promesa.core :as p])

(p/let [response (p/await (js/fetch url))
        data     (p/await (.json response))]
  (.log js/console data))

这种写法的优势在于完全依赖库实现,不涉及编译器插件的兼容性问题,且与现有 CLJS 工具链(shadow-cljs、lumic 等)无缝集成。但它本质上仍是 Promise 链的语法包装,无法获得原生 async 函数提供的栈追踪优化与语法简洁度。

从工程选型角度,以下参数可供团队评估:

维度 cljs-async-await Promesa + p/let core-async 通道
语法侵入性 编译器级别 库级别 库级别
目标环境 ES2017+ 任意 Promise 环境 任意 JS 环境
错误栈追踪 原生优化 依赖 Promise 实现 通道层面统一处理
生态互操作 直接调用 JS async 需要包装 需要桥接层
维护状态 实验性,单人维护 活跃社区支持 官方维护

实践建议

对于新启动的 ClojureScript 项目,若目标浏览器与 Node.js 版本普遍支持 ES2017(主流浏览器自 2018 年起已全面支持),团队可以考虑将 cljs-async-await 纳入开发依赖。启用前应完成以下检查清单:

首先,确认项目构建工具的编译器配置支持传递自定义宏命名空间。shadow-cljs 用户需要在 shadow-cljs.edn:compiler-options 中添加 :language-in :ecmascript-2017。其次,对现有代码进行静态扫描,标记所有包含 do 表达式且内部可能调用异步操作的代码位置,预估迁移成本。最后,建立端到端测试覆盖,确保异步错误处理路径在目标浏览器版本上行为一致。

如果项目对编译器扩展持保守态度,或需要同时支持较老的移动端浏览器,Promesa 组合 p/let 仍是目前最稳妥的选择。该方案在代码可读性与跨环境兼容性之间取得了良好平衡,且与 core-async 的互操作可通过 core.async/promise-chan 等桥接函数实现。

小结

ClojureScript 尚未在语言层面原生支持 async/await 语法糖,官方更倾向于通过 core-async 通道模型与 Promise 封装库提供异步抽象。社区驱动的实验性编译器扩展 cljs-async-await 提供了接近原生的写法,但受限于 CLJS 代码生成的边界情况。对于追求生产稳定性的团队,基于 Promesa 的库方案配合 p/let 宏能够在不修改编译器的前提下获得较好的异步编程体验。


参考资料

compilers

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com