Skip to Content
内部原理响应式图与 UI 树

响应式图与 UI 树

Ansiq 里有两棵不同的“图”:

  • 响应式依赖图
  • UI 树

这是一个很容易被误解,但又非常关键的设计点。

上一页已经解释了 reactive graph 本身:

  • signal
  • computed
  • effect
  • dirty scope 是怎么传播出来的

所以这一页不再重复那部分,而只关注另一个问题:

当 runtime 已经知道“谁脏了”之后,为什么还需要一棵独立的 UI 树?

UI 树负责什么

UI 树负责:

  • 当前有哪些节点
  • 哪个节点在哪个位置
  • 哪棵子树需要替换
  • 布局怎么重新计算
  • 哪些终端区域需要重画

它回答的是另外一个问题:

脏了之后,该怎么更新屏幕?

换句话说:

  • 响应式图回答“谁脏了”
  • UI 树回答“怎么变”

为什么要坚持两阶段模型

把 Ansiq 的更新过程压到最简单,就是两阶段:

第一阶段:响应式传播

回答:

  • 哪个 signal 变了
  • 哪些 scope 因此 dirty

第二阶段:runtime 更新

回答:

  • 哪棵子树 rerender
  • 哪些祖先 relayout
  • 哪些区域 redraw
  • 最终发什么 terminal patch

如果没有这条边界,系统很快就会变成一团:

  • 依赖关系层偷偷决定布局
  • widget 层偷偷决定 patch
  • runtime 不再是统一协调者

为什么不能把它们混起来

如果把响应式图和 UI 树混成一层,你很快会碰到这些问题:

  • debug 时看不清到底是依赖关系错了,还是布局错了
  • runtime 逻辑和组件逻辑互相污染
  • 很难对局部更新做清晰优化

所以 Ansiq 当前一直在刻意守这条边界。

一个简单的心智模型

你可以把它压成一条链:

signal/computed change -> dependency graph marks scopes dirty -> runtime collects dirty scopes -> subtree rerender -> partial relayout -> terminal patch

为什么这会让实现更稳

这种拆分让很多 runtime 优化都变得更自然:

  • dirty queue 化
  • subtree replacement
  • continuity contract
  • damage model

因为你不会试图让“依赖关系层”直接处理“终端 patch”,也不会让 UI 树反过来承担依赖传播。

为什么这对调试也很重要

这条边界不只是为了架构整洁,也直接决定 debug 体验。

当你遇到 bug 时,现在至少可以先问:

  • 是 signal/computed/effect 的依赖传播错了?
  • 还是 dirty scope 收集错了?
  • 还是 subtree replacement / relayout / redraw 出了问题?

如果响应式图和 UI 树深度混在一起,这三个问题会很快搅成一团。

这也是为什么 Ansiq 会不断重复同一句话:

  • reactive graph 是一层
  • UI tree 是另一层
  • runtime loop 是它们之间的协调层

下一步

继续阅读:

  1. 子树替换
  2. 连续性契约
  3. 局部重排与 damage model
Last updated on