首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >KuiklyUI 响应式系统是怎么更新 UI 的

KuiklyUI 响应式系统是怎么更新 UI 的

原创
作者头像
骑猪耍太极
发布2026-06-13 23:47:45
发布2026-06-13 23:47:45
300
举报

上一篇文章讲了页面从创建到首帧的完整过程:body() 在内存中构建 view 树,createBody() 把树翻译成 Native 渲染指令。那篇文章末尾留了一个问题——attr {} 块里如果读了 observable 字段,依赖关系会在 body() 阶段建立。建立之后呢?

页面已经创建完了,用户点一下按钮触发 count++,KuiklyUI 怎么知道该更新哪个 View?它不会重新构建整棵 UI 树,而是记录 attr {} 读过哪些 observable,状态变化时只重新执行相关的 attr 块。对于 textbackgroundColor 这类普通渲染属性,变化会通过 Bridge 下发给 Native;对于宽高、flex 这类布局属性,则会进入后续布局重算。

下面用一个例子一路走到底。


一、从一个属性设置留下的问题开始

假设业务代码里有一个计数器和一段显示文本:

代码语言:kotlin
复制
var count by observable(0)

override fun body(): ViewBuilder = {
    Text {
        attr {
            text("count = $count")
        }
    }
}

上一篇文章里说过,body() 执行期间 attr {} 把属性先存进内存,不产生 Bridge 通信。但 text("count = $count") 里的 $count 是一次 observable 的读操作,countgetValue 时会被拦截。也就是说,body() 不只是执行了 DSL,它同时埋下了响应式依赖。

现在页面渲染完毕,用户看到 count = 0,点按钮触发 count++。KuiklyUI 怎么知道这次增量对应的就是上面这个 Text?怎么知道只需要重新执行这一个 attr {}?要回答这个,需要先看 var count by observable(0) 到底做了什么。


二、读写的拦截与依赖收集

要让改了数据自动更新 UI 成立,需要回答两个问题:怎么知道数据变了,知道之后该更新哪里。

主流方案在第一个问题上走了不同的路。Vue 3 用 Proxy 拦截属性读写,Compose 用 Snapshot 系统追踪状态。KuiklyUI 用的是 Kotlin 委托属性:by observable(0) 编译后自动生成 delegate,读写都经过 getValuesetValue。读的时候通知这个属性被读了,写的时候通知这个属性变了。但 observable 只负责发信号,真正把信号转成依赖关系和 UI 更新的是 ReactiveObserver

下面两张图分别对应读路径(依赖收集)和写路径(通知更新)。

读路径:依赖收集
读路径:依赖收集

第一张图是 body() 阶段 attr {} 首次执行时的读路径。ReactiveObserver 打开收集开关后执行整个 attr 块,块里每读一次 observable 就记录一条 key。执行完关闭开关,把读集合关联到这个 attr 块上,写入映射表。

这个设计把业务侧的使用成本压得很低。业务代码不需要声明这个 Text 依赖 count,也不需要像某些框架那样手写依赖数组;只要在 attr {} 里自然读取了 count,框架就能在执行过程中把这层关系记下来。复杂度没有消失,只是从业务代码转移到了 ReactiveObserver 内部。

这里有一个需要单独说明的细节:key 是怎么生成的,怎么保证读和写拿到的是同一个 key。

每个 observable() 调用时,框架给它分配一个全局自增的唯一 ID,比如这个 count 拿到的是 5。读的时候,getValue 把 ID 和属性名拼在一起("5_count")作为 key,通知 ReactiveObserver 记录到读集合。写的时候,同一个 countsetValue 拿到的还是 ID=5、属性名="count",拼出来的还是 "5_count"。ReactiveObserver 用这个 key 到同一张映射表里查,找到的就是刚才读路径下建立的那些 attr 块。

这个 key 不只靠属性名,而是由 observable 委托实例分配出的 ID 加属性名组成。同一页面的不同 Pager 实例里,即使都有一个叫 count 的字段,它们也各自持有不同的 observable 委托实例,拿到的 ID 不同,最终生成的 key 也不同。

写路径:通知更新
写路径:通知更新

第二张图是 count++ 后的写路径。setValue 通知 ReactiveObserver,用同样的 key 查映射找到相关 attr 块,重新执行它们。重新执行时同样重新收集依赖,因为条件分支可能让下次读的是不同的 observable。

这里重新执行的不是一个抽象的全局函数,而是当初 attr {} 注册时捕获了当前 View 和 attr 对象的闭包。所以多个 View 即使依赖同一个 observable,也会各自回到自己的 attr 块里重新计算。

整个过程对业务代码透明:开发者没有手动声明依赖,读到什么就建立什么。


三、更新链路与粒度选择

把读写两段拼在一起,就是一次 count++ 从触发到 Bridge 下发的完整时序:

count++ 到 Bridge 下发的完整链路
count++ 到 Bridge 下发的完整链路

这和上一篇文章的 createBody() 对接:创建期是批量把整棵内存树翻译成 Native 指令,更新期是只把变化的属性增量下发。同一套 Bridge 通道,粒度不同。

这个粒度不是随意选的。Compose、SwiftUI、Flutter 更常见的做法是:状态变化后重新执行组件函数,框架在新的描述结果和旧状态之间做 diff / commit。在它们各自的运行时内部,这条链路不需要每次跨过 Kotlin 到 Native 的 Bridge 边界。

KuiklyUI 的下游不同。每一次更新都要穿越 Bridge,如果每次 count++ 都重新执行整个 body() 再翻译成 Bridge 指令,通信成本会在高频场景迅速膨胀。所以它选择更细的粒度:只重新执行依赖了变化的 attr 块。普通渲染属性会直接生成 SET_VIEW_PROP 下发,布局相关属性则会先改 FlexNode,再触发布局重算。代价是响应式系统需要维护依赖映射和时序约束,但在跨语言 Bridge 的约束下,这是比较自然的策略。

从设计上看,这里其实是在两个目标之间取平衡:业务侧仍然写普通 Kotlin DSL,不显式管理依赖;框架侧则尽量把更新范围压小,避免把一次很小的状态变化扩大成整棵树的重建。前者保证写法简单,后者控制 Bridge 边界上的成本。


四、同步触发的取舍

KuiklyUI 的更新链路是同步执行的。count++ 到 Bridge 下发属性,全程在一个调用栈内完成。好处是调用链清楚,调试时不需要跨微任务追踪。Vue 这类 Web 框架更常见的是异步批量更新,把 effect 放入微任务队列,多次变更合并成一次 re-render,匹配浏览器 DOM 渲染管线。

KuiklyUI 选同步路径,更多是为了让跨端更新链路保持直接。attr 重新执行后生成的是 Bridge 指令而非直接操作 UI。加一层异步调度,未必能减少单次状态变化的 Bridge 成本,还会引入调度时机和调试链路的问题。

这个选择也有代价。同一个同步步长里连续改多个 observable,可能触发多次 attr 重新执行和多次 Bridge 下发;条件分支改变依赖时,也需要在每次重新执行 attr 后重新收集依赖。实际业务中,一个交互事件通常只修改有限几个状态,这个开销多数时候是可控的,但它确实是细粒度同步更新要承担的成本。

所以同步触发不是单纯为了快,也不是说异步批量不好。它更像是和 KuiklyUI 这套 attr 级更新模型配套的选择:链路短、行为直接,框架内部少一层调度系统;代价是批量合并能力弱一些,需要业务和框架共同避免高频、密集的状态写入。


总结

observable 负责把属性的读写变成信号。每次读通知 ReactiveObserver 这个属性被读了,每次写通知这个属性变了。

ReactiveObserver 负责把读信号记录成依赖关系,把写信号转成 attr 重新执行。依赖在 attr 第一次执行时自动收集,更新触发后只重新执行相关的 attr 块。

Bridge 负责把重新计算后的渲染属性增量下发给 Native。这和创建期的 createBody() 是同一套通道:创建期是批量翻译整棵树,更新期是增量下发变化了的属性。布局属性变化后怎么重新算尺寸和位置,是下一篇 FlexLayout 要看的问题。

响应式系统解决的是哪些 attr 需要重新执行、哪些属性需要重新计算。它的设计取舍也比较清楚:业务侧保留普通 DSL 的写法,框架侧用依赖收集和 attr 级更新把变化范围收窄。这样写代码的人不需要手动维护依赖,跨端通信也不必因为一个字段变化就扩大成整棵树的更新。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、从一个属性设置留下的问题开始
  • 二、读写的拦截与依赖收集
  • 三、更新链路与粒度选择
  • 四、同步触发的取舍
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档