Skip to Content
内部原理Runtime 循环

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 在高层上大致会经历:

  1. 创建 App
  2. 进入 terminal session
  3. 调用 mount(...)
  4. 第一次 render(...)
  5. 进入主循环
  6. 在输入、消息、响应式变化下反复 update(...) / rerender
  7. 退出并恢复终端状态

可以把它粗略理解成:

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
Runtime loop 的主更新链
1input / async messages / reactive flush
2dirty scope collection
3subtree rerender 或 full rerender
4partial relayout
5invalidated regions
6framebuffer diff
7terminal 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 做的不是“谁先到就随便画一下”,而更像是:

  1. 接收输入事件和异步消息
  2. 路由输入,看看 widget 是否消费
  3. 把消息送进 update(...)
  4. flush_reactivity() 收集 dirty scopes
  5. 决定 full rerender 还是 subtree replacement
  6. 做 partial relayout
  7. 计算 invalidated regions
  8. 做 framebuffer diff
  9. 发 terminal patch

这条顺序最重要的价值不只是速度,而是:

  • 顺序明确
  • 边界明确
  • 出 bug 时知道应该先看哪一层

一个很重要的设计选择

Ansiq 目前的设计原则是:

  • 响应式系统只负责“谁脏了”
  • runtime 负责“脏了以后怎么更新屏幕”

runtime loop 正是这条边界的交汇点。

它一头接 reactive graph,一头接 terminal surface。
如果这层边界不稳定,Ansiq 很快就会变成一个又像 React、又像即时渲染、又像命令式 patch 的混合物。

runtime loop 在系统中的位置
1reactive graph 提供 dirty 信息
2UI tree 提供当前结构
3runtime loop 统一决定更新路径
4surface / terminal session 接收结果
5最终发出 terminal patches

读源码时应该看什么

如果你想从实现角度理解这一页,建议从这些位置看:

  • ansiq-runtime
    • run.rs
    • engine.rs
  • ansiq-surface
    • session.rs

读的时候最重要的不是记住每个函数名,而是看它们是不是仍然遵守这条边界:

  • 事件从哪里进入
  • dirty 从哪里收集
  • rerender 在哪里决定
  • patch 从哪里发出

这一页的结论

在 Ansiq 里,runtime loop 不是被隐藏起来的 plumbing。

它是一个真正的架构对象,因为它把:

  • 反应式更新
  • 树更新
  • 局部布局
  • 终端 patch

串成了一条统一、可调试、可文档化的链路。

如果你想继续理解“为什么 reactive flush 不直接导致整树重跑”,下一页建议读 响应式图与 UI 树

Last updated on