携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情
之前的文章,已经讨论过了 LaunchedEffect(LaunchedEffect到底为我们处理了什么问题?),也知道了它的使用场景。但其实类似的,还有 SideEffect 和 DisposableEffect,这些工具函数从意思上讲,可统称为“效应、效果”,用以帮我们完成特定场景下的任务。今天先来讨论下SideEffect。
SideEffect
从英文单词上来讲,SideEffect这个词的意思是“副作用”,而从这个工具函数的功能上来讲,它也确实像“副作用”一样:每一次(Re)composition,都将触发一次effect执行。
源码分析
上源码,来看看是怎么实现的。
fun SideEffect(
effect: () -> Unit
) {
currentComposer.recordSideEffect(effect)
}
很简单,effect 就是一个无参lambda,也就是我们的任务。
继续看:
// “变化”类
internal typealias Change = (
applier: Applier<*>,
slots: SlotWriter,
rememberManager: RememberManager
) -> Unit
internal class ComposerImpl(
// ...
// 变化列表
private val changes: MutableList<Change>,
// ...
): Composer {
// ...
override fun recordSideEffect(effect: () -> Unit) {
record { _, _, rememberManager -> rememberManager.sideEffect(effect) }
}
private fun record(change: Change) {
changes.add(change)
}
// ...
}
private class RememberEventDispatcher(
private val abandoning: MutableSet<RememberObserver>
) : RememberManager {
// ...
private val sideEffects = mutableListOf<() -> Unit>()
fun sideEffect(effect: () -> Unit) {
// 添加effect
sideEffects += effect
}
// 分发effect,执行任务
fun dispatchSideEffects() {
if (sideEffects.isNotEmpty()) {
sideEffects.fastForEach { sideEffect ->
sideEffect()
}
sideEffects.clear()
}
}
// ...
}
internal class CompositionImpl() : ControlledComposition {
private val changes = mutableListOf<Change>()
private val composer: ComposerImpl =
ComposerImpl(
applier = applier,
parentContext = parent,
slotTable = slotTable,
abandonSet = abandonSet,
// 传入列表参数
changes = changes,
composition = this
).also {
parent.registerComposer(it)
}
// ...
override fun applyChanges() {
synchronized(lock) {
val manager = RememberEventDispatcher(abandonSet)
try {
applier.onBeginChanges()
// Apply all changes
slotTable.write { slots ->
val applier = applier
// 循环changes
changes.fastForEach { change ->
change(applier, slots, manager)
}
changes.clear()
}
applier.onEndChanges()
// Side effects run after lifecycle observers so that any remembered objects
// that implement RememberObserver receive onRemembered before a side effect
// that captured it and operates on it can run.
manager.dispatchRememberObservers()
// 分发执行
manager.dispatchSideEffects()
if (pendingInvalidScopes) {
pendingInvalidScopes = false
observations.removeValueIf { scope -> !scope.valid }
derivedStates.removeValueIf { derivedValue -> derivedValue !in observations }
}
} finally {
manager.dispatchAbandons()
}
drainPendingModificationsLocked()
}
}
}
SideEffect通过当前的composer,调用 recordSideEffect 函数注册该effect。当Composition完成时,将触发“变化列表”中所有的“变化”,即调用applyChanges函数;在该函数内部,会循环changes,触发change回调,而这个回调就是ComposerImpl.recordSideEffect()注册的,即:rememberManager.sideEffect(effect)。
这时候,effect还并未执行,它只是被添加到RememberManager的实现类(RememberEventDispatcher)中,其中变量sideEffects持有一个effect列表。
那什么时候执行effect任务呢?答案是changes全部添加完毕后。如上,依然是在applyChanges函数里,changes循环结束添加后,即调用RememberEventDispatcher.dispatchSideEffects()方法,循环内部的effect列表并执行,完毕后清除所有。
整体来看,原理上来说还是相对简单的,关键点为:
- 注册任务
- 执行compose/recompose流程
- 完成后,扫描已注册任务,执行并销毁
总结就是:单次的监听任务执行
例子
来个例子看看,还是经典的点击计数案例:
@Composable
fun SideEffectTest() {
val clicked = remember { mutableStateOf(0) }
Log.d("set", "side effect before: ${clicked.value}")
SideEffect {
Log.d("set", "side effect called")
}
Log.d("set", "side effect after: ${clicked.value}")
Column {
Text(
text = "Clicked: ${clicked.value}", modifier = Modifier
.padding(16.dp)
.clickable {
clicked.value = clicked.value + 1
}
)
}
}
分别在SideEffect前、中、后都加入了日志打印。启动后:
2022-08-01 16:44:00.582 14256-14256 set com.jd.example.composeeditandime D side effect before: 0
2022-08-01 16:44:00.583 14256-14256 set com.jd.example.composeeditandime D side effect after: 0
2022-08-01 16:44:00.668 14256-14256 set com.jd.example.composeeditandime D side effect called
虽然是“前、中、后”的日志,可是实际却是“前、后、中”的打印,原因就是前文所说的 “Effect在Composition完成后触发”,任务将最后执行。
点击一下按扭:
2022-08-01 16:46:09.230 14256-14256 set com.jd.example.composeeditandime D side effect before: 1
2022-08-01 16:46:09.230 14256-14256 set com.jd.example.composeeditandime D side effect after: 1
2022-08-01 16:46:09.248 14256-14256 set com.jd.example.composeeditandime D side effect called
因为click计数,导致了Recomposition的发生,于是又一次执行组件更新,同样地,effect还是最后才执行
小结
按“副作用”的字面理解,SideEffect的功能就很好懂了:它就是compose流程的副作用 —— 一次composition,一次副作用执行。下篇继续来讨论下另一个Effect:DisposableEffect,同样实用。