ClojureScript 1.12.145: 为 Lisp 风格的 Web 带来 Async/Await

ClojureScript 团队宣布发布 1.12.145 版本,引入了生态系统中需求最受关注的功能之一:对 asyncawait 的原生支持。这次更新标志着 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 的尝试受到了以下因素的阻碍:

  1. 编译器复杂度:实现这一点需要对 ClojureScript 编译器进行深层的改动。包括 shadow-cljs 创建者在内的知名社区成员之前的尝试都已得出结论,这是一项艰巨的任务。
  2. 语言差异化:在保持与 Clojure(JVM 版本)的一致性与为 JS 运行时提供必要工具之间存在张力。由于 awaitclojure.core 中已经是关键字,因此在 ClojureScript 中引入这一特殊形式代表了一种经过计算的差异化,以服务于 Web 环境的具体需求。

结论

ClojureScript 1.12.145 不仅仅是一个语法更新;它是对现代 Web 开发需求的务实响应。虽然 core.async 的 CSP 模型仍然是处理复杂并发的强大工具,但原生的 async/await 为与 JavaScript 生态系统交互提供了一条轻量级、无摩擦的路径。对于构建现代前端的开发者来说,能够编写对目标平台而言感觉原生的异步代码,对于提高生产力和互操作性来说是一个巨大的胜利。

Sources