JavaScript 框架不约而同地走向了响应式编程,却没有走向同一种响应式编程。信号、计算值、存储、副作用、观察者——每个主流框架都有自己的依赖图实现、自己的追踪规则、自己的调度模型、自己的生命周期语义。把一段响应式逻辑从一个框架搬到另一个框架,意味着重写;让两个响应式库协同工作,通常就是它们根本看不见彼此的依赖。

TC39 正在推进的 Signals 提案试图解决这个割裂,方案是把一个集成好的响应式图原封不动地标准化:可写信号、自动依赖追踪、基于相等性的传播、观察者语义,一整套。这也许是对的,但还有一个方向值得考虑:标准化得更少,而不是更多。具体来说,只标准化一个失效协议和一个带显式依赖的惰性缓存派生原语,自动追踪、相等性判断、调度、所有权和副作用这些事,通通留给框架自己搞定。

这个更小的“基座”长什么样?一个源头不保存状态,它只是通知“有些东西可能变了”:

const changed = Reactive.createSource();
const total = Reactive.derive([changed], () => calculateTotal());
let count = 0;
const countChanged = Reactive.createSource();
count++;
countChanged.invalidate();
const label = Reactive.derive([countChanged], () => `Count: ${count}`);
label.get()

调用 label.get(),它会按需求值,否则就直接返回缓存。仅此而已。这套东西比完整的信号方案克制得多,但恰恰是这种克制,有可能让它成为更好的标准。

为什么更小反而更稳妥?

ECMAScript 的特性一旦落地,几乎就是永久性的。一个响应式架构被写进语言之后,所有框架要么拥抱它,要么绕着它走很多年。抽象边界在这里是生死线。

有些响应式概念确实是普适的:依赖可以失效、派生值可以惰性求值、成功的求值可以缓存、失效可以传递、消费者需要知道什么时候该重新检查、订阅需要能清理。然而另一些东西,在不同框架之间差异极大:自动还是显式依赖追踪、相等性语义是 Object.is、结构比较还是版本号、图的生命期和内存管理、所有权树、副作用的调度与批量处理、事务语义、渲染集成、错误边界。这些都不是共识,而是各自的选择。

把机制做成标准,把策略留给框架顶部的设计,恰好能划出这一刀:

框架策略层────────────────────────信号、仓库、代理自动依赖追踪相等性与版本控制副作用和调度所有权与批量处理渲染集成────────────────────────标准化基座失效协议惰性缓存派生依赖替换通知机制清理机制

有了这样一个共用基座,不同框架的响应式闭包就可以互操作。一个框架的派生值可以消费另一个框架的失效源,一个工具库的响应式原语不必被锁定在特定框架的依赖图里,跨生态的组织、分包、渐进迁移都会有更低的摩擦。

完全标准化的信号提案让整个社区站到同一条船上,但也可能让船本身变得沉重而难以转向。只标准化底层“接水管”的方式,就是把水压和流速留给各家自己调。这种克制的标准或许生存力更强——它不要求所有人用同一种方式思考响应式,只要求在传递“什么东西可能变了”这件事上,大家说同一种话。

克制不是没野心,而是清楚什么该统一,什么该留给上下文去决定。对 JavaScript 响应式编程的未来来说,这可能比一套大而全的规范更值得一试。