Effect之SideEffect的使用与原理分析

720 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

之前的文章,已经讨论过了 LaunchedEffectLaunchedEffect到底为我们处理了什么问题?),也知道了它的使用场景。但其实类似的,还有 SideEffectDisposableEffect,这些工具函数从意思上讲,可统称为“效应、效果”,用以帮我们完成特定场景下的任务。今天先来讨论下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列表并执行,完毕后清除所有。

整体来看,原理上来说还是相对简单的,关键点为:

  1. 注册任务
  2. 执行compose/recompose流程
  3. 完成后,扫描已注册任务,执行并销毁

总结就是:单次的监听任务执行

例子

来个例子看看,还是经典的点击计数案例

@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,同样实用。