ClojureScript 1.12.145: 为 Lisp 风格的 Web 带来 Async/Await
ClojureScript 团队宣布发布 1.12.145 版本,引入了生态系统中需求最受关注的功能之一:对 async 和 await 的原生支持。这次更新标志着 ClojureScript 开发者与现代 JavaScript API 交互方式的重大转变,向与 ECMAScript 2016 标准更无缝的集成迈进。
多年来,ClojureScript 用户一直依赖外部库或复杂的模式来处理异步操作。通过允许编译器生成原生的 JavaScript async 函数,ClojureScript 现在提供了一种一等公民的方式来处理 Promises 和异步流,而无需额外的依赖开销。
在 ClojureScript 中实现 Async 函数
新功能通过将函数提示为 ^:async 来触发。当编译器遇到此提示时,它会生成一个 JavaScript async 函数。在这些函数内部,可以使用 await 特殊形式来暂停执行,直到一个 Promise 被解析(resolved)。
请考虑以下实现:
(refer-global :only '[Promise])
(defn ^:async foo [n]
(let [x (await (Promise/resolve 10))
y (let [y (await (Promise/resolve 20)]
(inc y))
;; not async
f (fn [] 20)]
(+ n x y (f))))
这种语法也扩展到了测试框架,允许开发者更自然地编写异步测试:
(deftest ^:async defn-test
(try
(let [v (await (foo 10))]
(is (= 61 v)))
(let [v (await (apply foo [10]))]
(is (= 61 v)))
(catch :default _ (is false))))
互操作性的权衡:原生 vs. CSP
async/await 的引入在社区内引发了关于 Clojure 异步编程哲学的健康辩论。长期以来,ClojureScript 中并发的黄金标准是 core.async,它实现了通信顺序进程(Communicating Sequential Processes, CSP)。
一些社区成员认为,对于管理复杂状态和进程间的通信,CSP 模型更为优越。正如一位用户所指出的,将异步函数包装在 CSP 中通常被视为“处理此问题的更好方式”,因为 Clojure 已经拥有了一套成熟的模式。其他人则质疑引入 JS 风格的 async 关键字是否真的是一种升级,或者它是否会过于偏离将逻辑推向通道(channels)的 Clojure 核心哲学。
然而,这一变化的主要驱动力是互操作性。Clojure 调查显示,异步支持在 JS 互操作性所需的功能增强列表中占据主导地位。通过支持原生的 async/await,ClojureScript 消除了在与现代浏览器 API 和流行的 JavaScript 库(几乎完全基于 Promise)进行常见交互时对 Promesa 等库的需求。
克服技术障碍
实现这一功能的道路并非一蹴而就。根据社区见解,之前实现 async/await 的尝试受到了以下因素的阻碍:
- 编译器复杂度:实现这一点需要对 ClojureScript 编译器进行深层的改动。包括
shadow-cljs创建者在内的知名社区成员之前的尝试都已得出结论,这是一项艰巨的任务。 - 语言差异化:在保持与 Clojure(JVM 版本)的一致性与为 JS 运行时提供必要工具之间存在张力。由于
await在clojure.core中已经是关键字,因此在 ClojureScript 中引入这一特殊形式代表了一种经过计算的差异化,以服务于 Web 环境的具体需求。
结论
ClojureScript 1.12.145 不仅仅是一个语法更新;它是对现代 Web 开发需求的务实响应。虽然 core.async 的 CSP 模型仍然是处理复杂并发的强大工具,但原生的 async/await 为与 JavaScript 生态系统交互提供了一条轻量级、无摩擦的路径。对于构建现代前端的开发者来说,能够编写对目标平台而言感觉原生的异步代码,对于提高生产力和互操作性来说是一个巨大的胜利。