HMR vs signals:成品与原型¶
前几天看到了 signals 这个包,这是一个参考 Preact Signals 实现的 Python 反应式编程原语库。本文将其和 hmr 中的反应式引擎对照了一下,聊聊两者在核心设计和 API 上的差异。
signals 库目前只有 PyPI 链接,因为它的仓库似乎是 private 的(manzt/signals)。好在它发布在 PyPI 上的代码是可见的,我基于那个代码做了对比分析。包括这个 GitHub 仓库也是从它 sdist 中的 ci.yml 中找到的。是的,他的 sdist 中也打包了 .github 文件夹😂
TL;DR¶
- signals 是教学级原型,代码不到 500 行;HMR 反应式引擎更健壮,edge cases 处理更完善
- signals 的
batch在复杂依赖图下容易出脏节点;HMR 用 push-pull 调度和 invalidation 确保正确性 - signals 混用类和函数式 API;HMR 全函数式,命名一致,和 Python 习惯对齐。而且提供更丰富的原语类
- signals 只单线程;HMR 原生支持 asyncio/trio,还能隔离独立的反应式上下文
Batch:细节决定生死¶
signals 的批量更新很简单:全局 BATCH_PENDING 收集 listener,结束时逐个触发;若在 batch 里反复 set 同一个信号,PROCESSING_SIGNALS 只是防止简单的自触发循环,复杂依赖图下容易出现“部分节点没被标脏”或“脏了但没重算”的情况。
HMR 的调度是 push-pull 混合:信号只做 invalidation push,派生值在被读取时才 lazy recompute,并带缓存;批量更新结束时统一同步 dirty 链,确保拓扑顺序正确,还能跨 async 任务对齐状态。这种严谨的调度是应用在复杂依赖图中的基础。
API 心智负担¶
signals 既有 Signal/Computed/Effect 类,也提供 computed(fn)、effect(fn) 这样的函数式装饰器;还混入了 IPython 的 %%effect cell magic。对用户来说,范式切换频繁,生命周期谁管谁也容易糊涂。
HMR 直接走函数式:signal、derived、effect、async_derived、async_effect,加一个 batch。命名全小写,跟 Python 内置风格对齐;装饰器也可以柯里化,读写方式完全一致,用户学一套 API 就够了。
并发与上下文¶
signals 假定单线程:全局 CURRENT_COMPUTED、BATCH_PENDING,没有 sniff 当下的 async runtime,也没有上下文隔离,多个任务同时跑时很容易串状态。
HMR 内置 sniffio,能在 asyncio、trio 下正常工作;new_context() 让你在同一进程里跑多套独立的反应式系统,每套都有自己的全局栈,互不干扰。
原语的丰富度¶
signals 提供 Signal、Computed、Effect 三种原语;HMR 基于同样的核心设计,额外还提供了:
async_derived、async_effect:原生异步支持state、derived_method、derived_property:描述符属性语法糖memoized、memoized_method、memoized_property:作为 eager 版的 derivedreactive集合类型,对容器操作的反应式支持
reactive相当于 Vue 中的reactive({}),能让字典、列表等容器类型的变更也触发依赖更新。你能想象没有reactiveAPI 的 Vue 吗?目前只有 hmr 库实现了这个 API。连 reaktiv 都没有。
所有这些都在同一套 push-pull 内核上构建,不需要学习新的心智模型。
比如,你没看过文档,但猜猜下面的代码会输出什么?
from reactivity import state, derived_property, effect
class Rect:
width = state(10)
height = state(5)
@derived_property
def area(self):
return self.width * self.height
rect = Rect()
with effect(lambda: print(f"Area: {rect.area}")):
rect.width = 20
rect.height = 100000
答案:
- 先打印出 50,因为 effect 立即运行一次,10 × 5 = 50
- 然后打印出 100,因为把 width 改成 20 后,面积变成 20 × 5 = 100
- 最后一次设置 height 不会触发输出,因为 effect 已经被 dispose 了。是的,出了那个 block 就 without 这个 effect 了。反应式编程就像英语一样自然!
HMR 就是这么设计的:编程就应该像字面意思一样容易理解。
结语:原型 vs 生产级¶
signals 的代码不复杂。但真实应用会遇到循环依赖、并发竞态、异步协调、上下文隔离这些麻烦。HMR 把这些坑都踩过并填了,同时保持统一的 Pythonic API 风格。
想学习推拉反应式的原理?signals 足够。要自然、方便地使用?要搭建复杂的应用级反应式系统?HMR 更靠谱。