Runtime 循环
在很多 UI 系统里,主循环看起来像一个“底层细节”:
- 收事件
- 调 render
- 刷屏
但在 Ansiq 里,runtime loop 不是一个小实现细节,而是一条真正的系统边界。
原因很简单:Ansiq 不只是画一棵树,它要同时协调:
- terminal events
- app messages
- reactive flush
- rerender 决策
- layout 与 patch emission
这些事情如果没有一条明确的主循环来统筹,很快就会互相打架。
为什么 runtime loop 是一等对象
先看 Ansiq 现在真正要协调的对象:
- 输入事件
- 异步任务消息
- 响应式系统产生的 dirty scopes
- focus 和 routing
- subtree replacement
- partial relayout
- invalidated region tracking
- terminal patch emission
这意味着 runtime loop 不是:
“有事件就 render 一次”
而更像:
“把多个更新来源收敛成一条可预测的屏幕更新链”
先把生命周期和主循环分开
很多人第一次接触 Ansiq 时,会把下面两件事混成一件事:
App的生命周期- runtime 的每次更新循环
它们有关,但不是同一个层次。
App 生命周期
一个 Ansiq app 在高层上大致会经历:
- 创建
App - 进入 terminal session
- 调用
mount(...) - 第一次
render(...) - 进入主循环
- 在输入、消息、响应式变化下反复
update(...)/ rerender - 退出并恢复终端状态
可以把它粗略理解成:
create app
-> enter terminal session
-> mount(...)
-> first render(...)
-> main runtime loop
-> exit / restore terminal每次更新的 runtime loop
而 runtime loop 更细,它不断协调:
- 输入事件
- 异步消息
- reactive flush
- rerender 决策
- relayout
- redraw / diff / patch
所以生命周期回答的是:
app 在一生中会经历哪些阶段?
而 runtime loop 回答的是:
某一次变化发生后,系统如何把它稳定地落到终端上?
这条链到底是什么
今天的 Ansiq runtime 主链可以概括成:
input / async messages / reactive flush
-> dirty scope collection
-> subtree rerender or full rerender
-> partial relayout
-> invalidated regions
-> framebuffer diff
-> terminal patch emission这条链的价值,不只是性能,更是边界清晰。
mount / render / update 各自负责什么
mount(...)
mount(...) 适合做应用一启动就应该存在的事情,例如:
- 启动后台采样
- 建立长连接
- 发出初始消息
它不负责画 UI,而是负责把长期任务接进 runtime。
render(...)
render(...) 负责:
- 描述当前 UI 树
- 暴露组件边界
- 建立本轮的 reactive 读取
它不是直接往 terminal 写字符,也不负责低层 patch。
update(...)
update(...) 负责:
- 消费消息
- 修改 app state
- 决定是否继续启动异步工作
它不直接负责:
- relayout
- patch emission
- focus tree 的低层实现
如果压成一句话,可以记成:
mount建立长期背景条件,update消费事实并改状态,render描述当前 UI。
为什么不能把这些逻辑散到各层
如果没有一个强 runtime loop,最常见的后果就是:
- 输入处理自己决定何时 redraw
- widget 自己修改全局状态
- surface 自己决定 viewport 何时重锚
- app 逻辑自己偷偷做 rerender
这种系统在很小时候还能工作,但一旦涉及:
- 异步任务
- 多 pane
- subtree replacement
- history / scrollback
就会变得非常难 debug。
Ansiq runtime loop 的职责
当前文档里对 runtime 边界的正式定义已经写在:
但从 loop 角度看,可以再浓缩成三件事:
1. 收集所有“需要更新”的来源
包括:
- 键盘输入
- async 消息
- reactive dirty scopes
2. 决定最小可接受更新
包括:
- 是 full rerender 还是 dirty subtree replacement
- 哪些祖先需要 relayout
- 哪些区域只需要 redraw
3. 把更新稳定地提交给终端
包括:
- full frame patch
- region diff
- cursor patch
这也是为什么 runtime loop 不能只是“事件循环细节”。
它实际上决定了整个框架的更新模型。
一次真实更新在 loop 里怎么走
假设用户在输入框里敲了一个字符,同时后台任务也发回了一条 Message::Chunk。
runtime loop 做的不是“谁先到就随便画一下”,而更像是:
- 接收输入事件和异步消息
- 路由输入,看看 widget 是否消费
- 把消息送进
update(...) flush_reactivity()收集 dirty scopes- 决定 full rerender 还是 subtree replacement
- 做 partial relayout
- 计算 invalidated regions
- 做 framebuffer diff
- 发 terminal patch
这条顺序最重要的价值不只是速度,而是:
- 顺序明确
- 边界明确
- 出 bug 时知道应该先看哪一层
一个很重要的设计选择
Ansiq 目前的设计原则是:
- 响应式系统只负责“谁脏了”
- runtime 负责“脏了以后怎么更新屏幕”
runtime loop 正是这条边界的交汇点。
它一头接 reactive graph,一头接 terminal surface。
如果这层边界不稳定,Ansiq 很快就会变成一个又像 React、又像即时渲染、又像命令式 patch 的混合物。
读源码时应该看什么
如果你想从实现角度理解这一页,建议从这些位置看:
ansiq-runtimerun.rsengine.rs
ansiq-surfacesession.rs
读的时候最重要的不是记住每个函数名,而是看它们是不是仍然遵守这条边界:
- 事件从哪里进入
- dirty 从哪里收集
- rerender 在哪里决定
- patch 从哪里发出
这一页的结论
在 Ansiq 里,runtime loop 不是被隐藏起来的 plumbing。
它是一个真正的架构对象,因为它把:
- 反应式更新
- 树更新
- 局部布局
- 终端 patch
串成了一条统一、可调试、可文档化的链路。
如果你想继续理解“为什么 reactive flush 不直接导致整树重跑”,下一页建议读 响应式图与 UI 树。