96SEO 2026-08-13 08:00 0
不过,
User Pain Point:
很多人误以为 mutableStateOf 内部维护着 listener 列表。写值时遍历通知,State 对象根本不知道谁在观察它,它只是一个“能存多个版本的格子”。真正的模型是数据库的 MVCC。下面给出对应关系:

| 数据库概念 | Compose 对应概念 |
|---|---|
事务/快照ID = Snapshot.id | 行的多版本 = StateRecord 链表 |
The subscription layer is added by Compose on top of MVCC:
readObserver.User Pain Point: 初学者往往把隔离、订阅、通知三件事混在一起,导致难以理解。请把它们彻底分开:
┌─────────────────────────────────────────────────────┐ │ 问题 A:隔离 │ │ 不同线程/不同事务读到的值为什么互不干扰?│ │ → 靠 StateRecord 多版本链表 + snapshotId 可见性规则 │ ├─────────────────────────────────────────────────────┤ │ 问题 B:订阅 │ │ 谁读了谁,是怎么记下来的?│ │ → 靠 Snapshot.readObserver 回调 │ ├─────────────────────────────────────────────────────┤ │ 问题 C:通知 │ │ 改了值之后怎么找到该重组的 scope?│ │ → 靠 modified 集合 + applyObserver + 反查依赖表 │ └─────────────────────────────────────────────────────┘
User Pain Point: 想快速了解 Compose Snapshot 的主要原理,不必深究工程化细节。下面这份代码只有约 200 行,覆盖 A/B/C 三个问题。
/** 一个版本。真实源码里叫 StateRecord */
abstract class Record {
var snapshotId: Int = 0 // 写入该快照时产生此版本
var next: Record?= null // 单链表,头部最新
}
class IntRecord : Record
快照类
open class MiniSnapshot(
val id: Int。val invalid: Set,// 不可见的快照 ID 集合
val readObserver: -> Unit)?= null,val writeObserver: -> Unit)?= null,) {
companion object {
private var nextId = 0 // 当前打开、尚未提交的快照 ID
private val open = mutableSetOf// 全局快照
var global = MiniSnapshot。null)
private val threadLocal = ThreadLocal
/** 当前线程使用的快照 */
val current get = threadLocal.get?: global
/** 创建可变快照 */
fun takeMutable(
readObserver: -> Unit)?= null,writeObserver: -> Unit)?= null,): MutableMiniSnapshot {
val id = nextId++
val snap = MutableMiniSnapshot,readObserver。writeObserver)
open += id // 我看不见所有还开的事务
return snap
}
/** 在指定快照中执行代码块 */
fun enter -> T): T {
val prev = threadLocal.get
threadLocal.set
try { block } finally { threadLocal.set }
}
/** 提交后全局通知 */
val applyObservers =
mutableListOf< -> Unit>
}
}
class MutableMiniSnapshot(
id : Int,invalid : Set,readObs : -> Unit)?,writeObs : -> Unit)?): MiniSnapshot {
/** 本事务已修改过哪些状态 */
val modified = mutableSetOf
fun apply {
open -= id // 提交即把自己的 ID 从未提交集合移除
global = MiniSnapshot)
if )
applyObservers.forEach { it) }
}
}
关键一句: "提交不是把数据写回去。而是让自己的 ID 从 invalid 集合里移除,让已写好的版本变得可见"。
状态对象
class MiniState{
private var firstRecord: Record
= IntRecord.apply{ snapshotId=0 }
/* ----读取---- */
var value:Int
get{
val snap=MiniSnapshot.current
snap.readObserver?.invoke // ★ 订阅点
return as IntRecord).value
}
set{
val snap=MiniSnapshot.current as?MutableMiniSnapshot
: error
writable.value=v // ★ 写新版本
snap.modified += this // ★ 标记脏数据
snap.writeObserver?.invoke
}
/* ----可见性规则——找最新符合条件的记录——id <= snapshot.id 且不在 invalid 中——取最大的那个----*/
private fun readable: Record{
var candidate: Record?=null
var r= firstRecord
while{
if{
if
candidate=r }
r=r.next }
return candidate ?: error
}
/* ----写操作——若已有相同 ID 的记录则复用,否则新建——挂到链表头----*/
private fun writable:IntRecord{
val cur=readable as IntRecord
ifreturn cur // 已写过一次原地改
return IntRecord.also{ it.snapshotId=snapshot.id ; it.next=firstRecord ; firstRecord=it }
}
}
跑一下示例代码
fun main{
/* --- 问题 A:隔离 --- */
val count=MiniState
println // 外面能看到初始值
/* 开启事务并修改但未提交 */
val snap=MiniSnapshot.takeMutable
MiniSnapshot.enter{ count.value++ }
println // ← 外面看不到未提交修改
println{ count.value })// ← 在事务内能看到变化
snap.apply // 提交后外面可见更新
println
/* --- 问题 B+C:订阅与通知 --- */
val deps=mutableMapOf
val scopeName ="MyComposable"
/* 创建观测器并读取一次形成订阅关系 */
val obsSnap=MiniSnapshot.takeMutable(readObserver={state ->
deps.getOrPut{mutableListOf}+=scopeName })
MiniSnapshot.enter{
println // 建立订阅关系
}
obsSnap.apply
/* 注册全局 apply 通知,打印受影响 scope 列表 */
MiniSnapshot.applyObservers += { changed ->
changed.forEach{ s-> println } }
/*
修改并提交 */
MiniSnapshot.takeMutable.enter{
count.value++
}.apply
If you run this snippet you’ll see how Compose Snapshot stitches “consistent reads within a transaction” and “automatic dependency collection” toger.
四、订阅是怎么发生的:readObserver 全链路
1️⃣ 阅读入口 —— 把 State 和 Observer 分离开来
kotlin
// 在 Snapshot.kt 中:
fun T.readable : T{
val snapshot= Snapshot.current // 当前活动快照
/* ★ 唯一触发订阅点 — 把 stateObject 本身送进观察者 */
snapshot.readObserver?.invoke
return readable
: throw IllegalStateException
-
The observer receives whole StateObject,not just its value.
.
-
This makes dependency maps keyed by object reference rar than by value.
.
2️⃣ 谁装上 Observer 并发起读取?— Composition 与 Recomposer 的角色
kotlin
// CompositionImpl.composeContent
private fun composing->T): T{
/** 在 composition 周期内为当前块创建一个可变 SnapShot 并装上 Observer*/
val snap= Snapshot.takeMutable(
readObserveFn=this@CompositionImpl::recordReadOf,writeObserveFn=this@CompositionImpl::recordWriteOf )
try{ return snap.enter} finally{ applyAndCheck}
}
kotlin
// Composer.recordReadOf
internal fun recordReadOf{
if{
composer.currentRecomposeScope?.let { scope->
scope.used=true
observations.add }}
}
-
The currentRecomposeScope is usually closest @Composable function that can be skipped on recomposition.
.
-
This explains *** reading inside an inline lambda causes its parent composable to be re‑executed.
.
🔎 存依赖的数据结构 — ScopeMap 与 IdentityHashMap 差异
-
Keeps dependencies keyed by identityHashCode for speed and correctness with data classes.
.
-
Saves dependencies in sorted arrays for binary search – no per‑node allocation.
.
-
Satisfies one‑to‑many mapping and reverse removal when scopes become invalid.
.
五、通知是怎么发生的:apply 全链路
1️⃣ 写入入口 – policy.equivalent 拦截相等性检查
kotlin
override var value:T set{
if){ //<-- 相等性策略拦截
writable{this.value=value} //<-- 新建或复用记录
}
}
-
The default policy uses structural equality;same reference assignment won’t trigger updates—*** mutating an ArrayList inside mutableStateOf doesn’t refresh UI unless you replace it entirely.
.
• Three policies exist:
- structuralEqualityPolicy : default
- referentialEqualityPolicy : useful for heavy data classes
- neverEqualPolicy: always treat values unequal so every assignment triggers an update ..
2️⃣ 提交与全局广播 — registerApplyObserver / GlobalWriteWatcher
kotlin
// Registering an observer that will be called after every commit.
val unregisterApplyObserves =
Snapshot.registerApplyObserver{changed:Set,_ ->
synchronized{
if{
snapshotInvalidations.add;deriveStateLocked;//<-- wake recomposer coroutine
}}
}
// Later we call:
val changed:Set,changed.forEach{ s->println }
// The Recomposer marks slots corresponding to se scopes as dirty.
**Why callbacks still work after a click?**
The click handler runs in Global snapshot where no explicit `apply` is called. The `GlobalSnapshotsManager` watches for any global writes and schedules a single `commitPending` batch that eventually calls `sendApplyNotifications` which advances global ID and triggers all registered observers. Thus multiple writes inside one event are merged into a single notification.
*Tip*这方面。In your code never call `state.update` directly from background threads without a transaction—write operations go to a local snapshot ensuring consistency and atomicity.
🔧 Conflict detection & merge
⚙️ Detecting conflicts
During `MutableSnapShot.apply` each mutated state checks three versions:
-
current – latest globally visible version
-
previous – version seen when this transaction forked
-
applied – new version written by this transaction
If current ≠ previous,anor transaction modified this state after we forked → conflict.
🔁 Merging strategy
kotlin
val merged=
state.mergeRecords
// Default implementation:
override fun mergeRecords: StateRecord?{
return if){
current //<-- keep current if equal
} else policy.merge
?.let{/* generate new record */}
}
If merge returns null,commit fails .
Snippet: Snippet implements merge logic based on operation sequence numbers enabling automatic conflict resolution for concurrent appends.
🚀 Practical use cases
kotlin
@Composable
fun EditForm{
var draft by remember{mutableStateOf<>}
Button(onClick={draft=
Snapshot.takeMutable{/* editing changes */}}){Text}
Button;
draft=null}){Text}
Button;draft=null}){Text}
or more concise using syntax sugar:
kotlin
withMutableSnapShot{
account.balance-=50
order.status=Paid
} //<– exception rolls back all changes automatically.
📚 附:一页速查表
Compose
#1 immutable vs mutable containers #2 derived vs remember #3 graphicsLayer vs Modifier.drawBehind etc #... #N debugging tips #X more advanced concepts…etc,#…,#Z …etc.. etc.. etc..etc…etc,etc…. etc..etc..etc…etc,etc…. ,etc…. ,#Y …等等,…,…等等,…等等,…等等,…,…,…,…,…,…,…,等等…,…,…,…,…,…,…,…,等等…,…,…,…,等。. . . . . . . . . .. ... .. .. .. .. ...... .... ......... .. .... .......... ................ .................... .................... .................... ....................' />
⚡️ 每行都可能会出现 bugs 或性能问题,请务必仔细阅读源码或官方文档。⚡️⏱️🛑💬🚀💭📈📉🧩🧪🔬💻📱⛵️🚨🤔😕😳👻👽🤖👾🙈🐱🚀 🗺️⚙️🛠️❓✔️✖️✅🙆♀️🙇♂️🏃♀️🏃♂️🌟✨🔥🚀🌈⚡☀☁🌧💡🔋🔌🗂⌨⌚📷📺🎬🎭🎨🎶🎵💬📝⚖✍💡🔍🔎🥇🥈🥉🏆🏅👑👑🍰🍕🍔🍟🍣🍜🥗🥪🍝🐟🐶🐱🐶🐼🐸🐒🐯🐸🦁🦈🦅🦜♓♠♥♦♣⚔☘☀☂♞ ♙✝︎✡︎ ☪ ✲✴ ✲☆★◆◇□▲△▼▽◉◊○◎⊙●◎◎◎◉◊■▢▣▤▥▦ ▧ ▨ ▩▰▱▬▸▶↗↘↙↖⇑⇓⇐⇒↔↕↖ ↫ ⭢⭣⭤⭥ ⬆⬇⬅➡↩↪➜➝➞ ➲ ➿ ➵ ➴ ➫ ➬ ➫ ⬆︎︎︎' />
#1 可变容器不会自动触发 UI 更新吗? 仅当你将整个容器替换掉时才会触发,因为内部引用没变。说起来,使用 @Stable @Immutable @OptIn 或 mutableStateListOf` 来获得更细粒度更新。
#2 为什么 derivedStateBy 会比 remember 更高效? 因为 derived 缓存结果,而且只有当其依赖真正变化且结果不同才会触发外层 recomposition。老实说,
#... 更多技术细节请查看官方源码或社区博客。
作为专业的SEO优化服务提供商,我们致力于通过科学、系统的搜索引擎优化策略,帮助企业在百度、Google等搜索引擎中获得更高的排名和流量。我们的服务涵盖网站结构优化、内容优化、技术SEO和链接建设等多个维度。
| 服务项目 | 基础套餐 | 标准套餐 | 高级定制 |
|---|---|---|---|
| 关键词优化数量 | 10-20个核心词 | 30-50个核心词+长尾词 | 80-150个全方位覆盖 |
| 内容优化 | 基础页面优化 | 全站内容优化+每月5篇原创 | 个性化内容策略+每月15篇原创 |
| 技术SEO | 基本技术检查 | 全面技术优化+移动适配 | 深度技术重构+性能优化 |
| 外链建设 | 每月5-10条 | 每月20-30条高质量外链 | 每月50+条多渠道外链 |
| 数据报告 | 月度基础报告 | 双周详细报告+分析 | 每周深度报告+策略调整 |
| 效果保障 | 3-6个月见效 | 2-4个月见效 | 1-3个月快速见效 |
我们的SEO优化服务遵循科学严谨的流程,确保每一步都基于数据分析和行业最佳实践:
全面检测网站技术问题、内容质量、竞争对手情况,制定个性化优化方案。
基于用户搜索意图和商业目标,制定全面的关键词矩阵和布局策略。
解决网站技术问题,优化网站结构,提升页面速度和移动端体验。
创作高质量原创内容,优化现有页面,建立内容更新机制。
获取高质量外部链接,建立品牌在线影响力,提升网站权威度。
持续监控排名、流量和转化数据,根据效果调整优化策略。
基于我们服务的客户数据统计,平均优化效果如下:
我们坚信,真正的SEO优化不仅仅是追求排名,而是通过提供优质内容、优化用户体验、建立网站权威,最终实现可持续的业务增长。我们的目标是与客户建立长期合作关系,共同成长。
Demand feedback