SlideEffect排在第三位执行,一般是函数中最后一行同步代码执行完,再执行。
2025-12-26 14:22:10.974 6006-6006 Exp4 com.example.composestudy D 1️⃣ 函数体:data=初始
2025-12-26 14:22:10.974 6006-6006 Exp4 com.example.composestudy D 3️⃣ 函数体继续:data=初始
2025-12-26 14:22:10.980 6006-6006 Exp4 com.example.composestudy D 2️⃣ SideEffect 立即执行:data=初始
先给一个绝对准确的结论(可以直接记住)
SideEffect 的代码是在 Composable 函数执行"期间被记录",
但真正执行是在:本次重组完成、UI 变更已提交之后。
所以答案是:
- ❌ 不是"函数最后一行执行完立刻执行"
- ❌ 也不是"UI 已经绘制到屏幕之后"
- ✅ 是"Compose 完成一次成功重组并提交变更之后,在同一帧内执行"
一、为什么日志"看起来像顺序执行"?(这是关键)
看到的是类似这种日志顺序,对吧 👇
kotlin
@Composable
fun Demo() {
Log.d("TAG", "A")
SideEffect {
Log.d("TAG", "SIDE")
}
Log.d("TAG", "B")
}
日志是:
text
A
B
SIDE
或者有时会感觉:
"SideEffect 好像就是在函数里顺着跑的"
原因只有一个:
SideEffect 在函数执行时"被注册",不是立即执行
Compose 在函数执行阶段做的是:
text
执行 Composable 函数
├─ Log("A")
├─ 注册一个 SideEffect(放进 effect 队列)
├─ Log("B")
函数执行结束
👉 此时 SideEffect 还没执行
二、真正的时间线(非常重要,给"底层级"顺序)
我们用一个真实的时序图来看一次重组:
text
① 执行 Composable 函数(重组)
├─ 普通代码立即执行(Log、if、remember)
├─ SideEffect 只是被"登记"
└─ UI 描述树被重新计算
② Compose 内部:
├─ 对比新旧 UI 树(diff)
├─ 确认这次重组是"成功的"
└─ 提交 UI 变更(commit)
③ 执行 SideEffect(同步)
👉 SideEffect 永远在第③步
三、那为什么不是"UI 绘制完成之后"?(非常关键的区分)
Compose 有三层概念,很多人混在一起了:
| 层级 | 含义 |
|---|---|
| 重组(Recomposition) | 重新计算 UI 描述 |
| 提交(Commit) | 应用 UI 变更 |
| 绘制(Draw) | 真正画到屏幕 |
SideEffect 的位置是:
text
重组完成 ✅
提交完成 ✅
绘制开始 ❌ / 或正在同一帧调度中
SideEffect 不保证"画完了"
它保证的是:UI 状态已经稳定,不会被回滚
四、为什么 Compose 要这么设计?(设计动机)
核心原因一句话:
副作用是不可回滚的,而 UI 计算是可回滚的
如果:
- UI 还在计算
- 中途又被新的状态打断
👉 那这次 UI 计算会被丢弃
👉 但 SideEffect 如果已经执行,就会造成世界状态错乱
所以 Compose 的保证是:
只有当这次重组"确定要生效"时,才执行 SideEffect
五、再回答那句非常关键的直觉判断
"SideEffect 的执行似乎没有受到其他干扰"
这是对的,而且是刻意设计的
- 它不会被中途 cancel
- 不会被 recomposition 打断
- 不受 key 控制
- 只要这次重组提交了,就一定会执行一次
但注意 ⚠️:
每一次成功重组 → 执行一次 SideEffect
六、用一句"工程级"话术帮彻底定型
以后可以这样理解、这样跟别人说:
SideEffect 不是 Composable 代码的一部分,
而是"在这次 UI 更新已经确定之后,对外部世界做的同步动作"。
七、和 LaunchedEffect / DisposableEffect 的时序对比(加深理解)
| API | 注册时机 | 执行时机 |
|---|---|---|
| SideEffect | Composable 执行时 | 重组提交后(同步) |
| LaunchedEffect | Composable 执行时 | 重组提交后(启动协程) |
| DisposableEffect | Composable 执行时 | 进入 / 离开组合 |
八、现在已经到了 Compose 的「内核理解层」
能问出这个问题,说明已经在思考:
- Compose 到底什么时候"算 UI"
- 什么时候"影响世界"
这已经不是 API 层的问题了,是调度模型。
二 示例
@Composable
fun ExperimentScreen4() {
var data by remember { mutableStateOf("初始") }
Log.d("Exp4", "1️⃣ 函数体:data=$data")
// 同步 - 立即执行
SideEffect {
Log.d("Exp4", "2️⃣ SideEffect 立即执行:data=$data")
data = "SideEffect 修改" // 这行不会触发重组
}
// 异步 - 后台执行
LaunchedEffect(Unit) {
Log.d("Exp4", "4️⃣ LaunchedEffect 后台执行开始")
delay(100)
Log.d("Exp4", "5️⃣ LaunchedEffect 延迟后:data=$data")
}
Log.d("Exp4", "3️⃣ 函数体继续:data=$data")
Text("当前数据:$data")
}
2025-12-26 14:22:10.974 6006-6006 Exp4 com.example.composestudy D 1️⃣ 函数体:data=初始
2025-12-26 14:22:10.974 6006-6006 Exp4 com.example.composestudy D 3️⃣ 函数体继续:data=初始
2025-12-26 14:22:10.980 6006-6006 Exp4 com.example.composestudy D 2️⃣ SideEffect 立即执行:data=初始
2025-12-26 14:22:11.087 6006-6006 Exp4 com.example.composestudy D 4️⃣ LaunchedEffect 后台执行开始
2025-12-26 14:22:11.091 6006-6006 Exp4 com.example.composestudy D 1️⃣ 函数体:data=SideEffect 修改
2025-12-26 14:22:11.091 6006-6006 Exp4 com.example.composestudy D 3️⃣ 函数体继续:data=SideEffect 修改
2025-12-26 14:22:11.095 6006-6006 Exp4 com.example.composestudy D 2️⃣ SideEffect 立即执行:data=SideEffect 修改
2025-12-26 14:22:11.188 6006-6006 Exp4 com.example.composestudy D 5️⃣ LaunchedEffect 延迟后:data=SideEffect 修改