跳转至

HMR vs signals:成品与原型

HMR   ·   HMR Docs   ·   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 直接走函数式:signalderivedeffectasync_derivedasync_effect,加一个 batch。命名全小写,跟 Python 内置风格对齐;装饰器也可以柯里化,读写方式完全一致,用户学一套 API 就够了。

并发与上下文

signals 假定单线程:全局 CURRENT_COMPUTEDBATCH_PENDING,没有 sniff 当下的 async runtime,也没有上下文隔离,多个任务同时跑时很容易串状态。

HMR 内置 sniffio,能在 asyncio、trio 下正常工作;new_context() 让你在同一进程里跑多套独立的反应式系统,每套都有自己的全局栈,互不干扰。

原语的丰富度

signals 提供 Signal、Computed、Effect 三种原语;HMR 基于同样的核心设计,额外还提供了:

  • async_derivedasync_effect:原生异步支持
  • statederived_methodderived_property:描述符属性语法糖
  • memoizedmemoized_methodmemoized_property:作为 eager 版的 derived
  • reactive 集合类型,对容器操作的反应式支持

reactive 相当于 Vue 中的 reactive({}),能让字典、列表等容器类型的变更也触发依赖更新。你能想象没有 reactive API 的 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 更靠谱。