面向 LISP 开发者的 RSC
June 1, 2025
LISP 的一个重要理念是“代码即数据,数据即代码”。虽然这在某种程度上普遍成立,但在 LISP 里,这一点在文化和语法层面都被极力强调。比如,来看这样一段 LISP 代码:
(+ 2 2)
这会得到 4
。
但如果我们在前面加个引号:
'(+ 2 2)
结果突然变成了 (+ 2 2)
。
呃,这有什么用?其实,这就是一段 LISP 代码本身。对 LISP 代码“引用”(quoting)意味着“不要真的去计算它,只是把代码本身返回给我”。
当然,我可以稍后再去计算它:
(eval '(+ 2 2))
这又会得到 4
。
这就是为什么说“代码即数据”深深植根于 LISP 文化。LISP 语言本身就有一等公民的“不要执行这段代码”的原语,这就是引用。
现在来看看 Web 应用。
Web 服务器其实是一个用来生成另一个程序的程序。服务器生成客户端程序(用 HTML 和 JavaScript 编写),然后把它发送给客户端电脑。生成并发送代码,这听起来和“引用”很像。
在 JavaScript 里,我们没有引用的概念。我不能在函数前加个 '
,然后说“我现在想把它当作数据而不是代码来处理”。当然,我可以把它包进字符串字面量里,但这样就没有语法高亮,而且会丢失太多语法特性。你肯定不想在字符串里写代码。
在 JavaScript 里,我们无法“引用”某个代码块而不损失语言的诸多优势。但如果我们能“引用”……整个模块呢?
React Server Components(RSC)是一种客户端-服务器编程范式,它用类似的思路,让服务器端代码能够引用客户端代码。'use client'
指令让你导入专为客户端设计的代码——但不会立即执行它:
'use client'
export function onClick() {
alert('Hi.');
}
这和引用类似,都是把一段代码当作数据来处理。不同的是,在 LISP 里你可以操作和变换被引用的代码,而在这里你拿到的是一个“黑盒”——你无法变换或检查这段代码。
这意味着,后端代码里导入 onClick
时,拿到的并不是真正的 onClick
函数,而是像 '/js/chunk123.js#onClick'
这样的标识符,用来指示如何加载这个模块。这就是“代码即数据”。不过和 LISP 的引用不同,这个机制是在编译时由打包工具实现的。
最终,这段代码会作为 <script>
送到客户端,并在那里被真正执行。到那时,onClick
函数才会实际存在(也许还会被调用)。
这样一来,我们就能以非常模块化的方式,编写在不同阶段(服务器端和客户端)执行的组合行为的程序。可以参考这个例子。 “引用”之外的部分处理服务器专属资源,“引用”之内的部分则是有状态、存在于客户端的代码——但它们可以组合。服务器端的代码可以包裹客户端代码,客户端代码也可以包裹服务器端代码,只要所有组合都是在服务器端完成的。而在服务器端做组合,可以保证所有服务器端逻辑都在单次请求/响应周期内完成。而且,这也是渐进式流式传输的。
其实大致就是这样。当然,这比起 LISP 的引用要弱得多,因为代码的执行策略是由 React 规定的,没法像 LISP 那样做元编程、变换代码本身。所以也许这个类比还是有点牵强。
我知道 LISP 在跨多环境组合代码方面有丰富的传统,一些新方案比如 Electric 也逐渐流行起来。可惜我对 LISP 了解还不够深入,无法细致探讨这些内容,但我很希望能看到更多面向 JavaScript 开发者的讲解,无论是以往的经验还是新想法。
谢谢!
我也会努力学学 LISP 的。