响应式
Ansiq 当前推荐的响应式原语是:
signalcomputedeffect
如果你来自 React 背景,最值得先切换的心智是:
- Ansiq 不以“整组件重跑”为核心
- Ansiq 更接近 Vue / Solid 的细粒度响应式
- 响应式系统负责“谁脏了”,runtime 负责“怎么更新终端”
继续沿用 NotesApp
在 第一个应用 里,我们已经有了一个最小可用的输入与提交应用。
现在我们不换例子,而是在它的基础上加两件事:
- 一个派生值:当前输入长度
- 一个副作用:输入清空时触发响应式 effect
fn render(&mut self, cx: &mut ViewCtx<'_, Self::Message>) -> Element<Self::Message> {
let draft = cx.signal(|| String::new());
let current = draft.get();
let input_len = cx.computed({
let draft = draft.clone();
move || draft.get().chars().count()
});
cx.effect({
let draft = draft.clone();
move || {
if draft.get().is_empty() {
// 这里用来示意 effect 的角色:
// 做副作用,而不是直接生成 UI
}
}
});
view! {
<Paragraph text={"Type and press Enter"} />
<Input
value={current.clone()}
on_change={{
let draft = draft.clone();
move |next| draft.set_if_changed(next)
}}
on_submit={|next| Some(Message::Submit(next))}
/>
<Paragraph text={format!("Characters: {}", input_len.get())} />
<Paragraph text={format!("Last submit: {}", self.submitted)} />
}
}这时同一个小应用已经开始展示响应式分层:
draft是基础状态input_len是派生状态effect是副作用边界
signal
signal 是最基础的可变响应式状态。
它适合:
- 输入框值
- 当前选中项
- pane 展开/收起
- 会被多个 UI 部分读取的小状态
在 NotesApp 里,draft 就是最典型的 signal:
- 它表示“当前正在编辑、但还没提交”的局部值
- 它需要被
Input和派生值同时读取 - 它不需要立刻变成 app 的持久业务状态
computed
computed 表示派生值。
它适合:
- 基于多个 signal 组合出的展示值
- 已排序/已过滤的结果
- 纯展示导向的派生状态
在 NotesApp 里,input_len 是一个很典型的 computed:
- 它依赖
draft - 它不该自己存一份副本
- 它只是一个派生展示值
effect
effect 用来表达“依赖变化时发生的副作用”。
这里要特别强调:
effect不同于 React 的useEffect。
它没有依赖数组,依赖由执行时的读取自动追踪。
它适合:
- 响应式驱动的后台行为
- 需要与 runtime、外部系统、任务协作的副作用
- 不应直接编码进 UI 表达式的逻辑
一个很好用的区分规则是:
- 会显示在 UI 上的派生值,优先考虑
computed - 会触发行为、任务、日志、同步动作的,优先考虑
effect
响应式和 runtime 的边界
这一页最重要的不是 API 名字,而是边界:
- 响应式系统决定 谁是 dirty 的
- runtime 决定 dirty 之后怎么局部更新终端
这也是为什么 Ansiq 的 reactive graph 和 UI tree 必须保持分层。
为什么这种模型适合终端应用
终端应用里最常见的变化往往都很局部:
- transcript 追加一段
- 某个 footer 数值变化
- 某个列表选中项变化
- 某个输入框多了一个字符
这些变化都不值得整树重建。细粒度响应式的价值就在这里:它帮 runtime 先把影响范围缩小。
如果你愿意把这个例子再往前推一步,可以先自己问一个问题:
- 当前 draft 变了,真的有必要让整个 app 都重新组织吗?
Ansiq 给出的答案就是:不必。先把 dirty scope 缩小,再把渲染范围缩小。
下一步
Last updated on