如果响应式效应仅仅是可暂停的异步任务,会怎样?

发布日期:2026-05-13 10:01:35   浏览量 :6
发布日期:2026-05-13 10:01:35  
6

2026西湖龙井茶官网DTC发售:茶农直供,政府溯源防伪到农户家 

我一直在从运行时层面思考响应式编程。问题是:要在 Rust 的异步/等待机制之上构建一个可用的响应式系统,你实际上需要添加的最少机制是什么?

Rust 已经免费提供的三样东西

  1. Pin> = 一个挂起的计算

试想一下:一个响应式效应就是“一段暂停的代码,等待其依赖项发生变化,然后重新执行。”这正是卡在 Poll::Pending 状态的未来(Future)。

scope.spawn(async move {
loop {
count.changed().await; // 在此处挂起
println!("count = {}", count.read());
}
});

SignalChangedFuture::poll() 检查一个单调递增的版本号。如果版本号未变,它会订阅一个回调并返回 Poll::Pending。异步执行器轮询其他任务。当信号发生变化时,版本号增加,回调被触发,唤醒器(Waker)唤醒,执行器重新轮询。这就是整个效应的生命周期——无需自定义调度器,无需遍历效应图,也无需 create_effect() 抽象。

执行器的轮询循环本身就是效应调度器。你不需要单独的调度器。

  1. Waker = 通知分发器

响应式库必须决定“当信号 X 变化时,哪些效应需要重新运行。”在异步模型中,这直接映射为“哪些未来(Future)应该被重新轮询。”唤醒器(Waker)就是这种分发机制:

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> PollSelf::Output {
if version_changed {
return Poll::Ready(self.signal.read());
}
// 订阅一个调用 cx.waker().wake_by_ref() 的回调
// 执行器在下一次刷新时重新轮询此未来
Poll::Pending
}

没有单独的通知队列。没有对依赖项进行拓扑排序。只是“唤醒唤醒器,执行器接手处理。”

  1. Drop = 清理

丢弃拥有这些任务的作用域 → 所有未来(Future)被丢弃 → 每个未来的 Drop 实现主动从其信号中取消订阅 → 零悬空订阅者。无需手动的 on_cleanup() 令牌,无需效应处置的记录管理。Rust 的所有权机制完成了这项工作。

对于深层嵌套的作用域(想想具有数百层级的用户界面组件树),取消操作使用广度优先搜索来收集所有后代,然后从叶节点到根节点进行迭代——没有递归丢弃,不会栈溢出。

Rust 未提供的 20%:重入防止

纯异步模型在此处失效。如果 Signal::set() 同步调用订阅者回调:

set() → 回调 → 对同一信号调用 set() → 回调 → 无限循环
set() → 回调 → 订阅新回调 → 在迭代期间修改订阅者列表 → RefCell 恐慌
set() → 回调 → 丢弃拥有作用域 → 借用冲突

仅靠异步模型无法防止这种情况。你需要一个延迟通知的状态机。

方法:Signal::set() 从不直接调用回调。它增加版本号,快照订阅者列表,并将一个闭包推入执行器的延迟回调队列。执行器在每次刷新的开始时、轮询任何任务之前排空此队列。

三种状态,两个标志:

notifying = false, dirty = false → 正常。快照订阅者,推送通知,设置 dirty。
notifying = true → 回调期间的重入 set()。仅设置 dirty,不产生新通知。
dirty = true, notifying=false → 通知已排队。无操作。

回调完成后:检查 dirty。如果在回调轮次中发生了重入 set(),则使用新的订阅者快照安排后续通知(以便包含回调轮次中新添加的订阅者)。

这涵盖了每种边缘情况:重入 set、回调迭代期间的订阅/取消订阅、作用域

免责声明:本文内容来自互联网,该文观点不代表本站观点。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,请到页面底部单击反馈,一经查实,本站将立刻删除。

关于我们
热门推荐
合作伙伴
免责声明:本站部分资讯来源于网络,如有侵权请及时联系客服,我们将尽快处理
支持 反馈 订阅 数据
回到顶部