D25 / 为什麽 State 改变会触发 recomposition - State & Snapshot system

今天大概会聊到的范围

  • Snapshot system

上一篇有提到,State 改变时会触发 recomposition。视这个行为是一种定义。但是为什麽?这怎麽发生的呢?

今天要介绍的是 Compose 中另一个与 State 息息相关的概念 - Snapshot

什麽是 Snapshot

Snapshot 可以想像是当下所有 State 的存档、快照。就像游戏存档或是电脑备份一样,当下是什麽就是什麽,全部存起来。

提醒:Snapshot 通常不会用在 Compose 的开发中,主要是在用於 Compose 的内部运作。

让我们看看 Snapshot 实际上怎麽使用的:

fun main() {

    // 1. 
    val data = mutableStateOf("") 
    
    // 2.
    data.value = "Foo"
    println("2: ${data.value}")
    
    // 3.
    val snap = Snapshot.takeSnapshot()
    
    // 4.
    data.value = "Bar"
    println("4: ${data.value}")
    
    // 5. 
    snap.enter {
        println("5: ${data.value}")
    }

    // 6.
    println("6: ${data.value}")
    
    
    // 7.
    snap.dispose()
}

Snapshot 和 State 的运作,是不一定要在 composable function 中执行的

Output:

2: Foo
4: Bar
5: Foo
6: Bar
  1. 透过 mutableStateOf 建立 State。
  2. 编辑 State 的 value 後,印出来的也不意外是 "Foo"
  3. 在这时我们拍一张 sanpshot,将当下的 State 通通存起来
  4. 再次编辑 State 的 value,并可以看到 State 的确有被我们修改
  5. 透过 Snapshot.enter 可以 access 当时拍的 snapshot,在这个 scope 内,我们可以取得当时的 state
  6. 在 snapshot 外,state 依然是编辑後的 Bar,并不会因为我们进去 snap 就改变
  7. 使用完 snapshot 机制後,要将其 dispose 释放掉资源

Snapshot 的概念就是这麽单纯:保留某一个时刻的所有 State。

MutableSnapshot

在 enter snapshot 後,如果我们还想对 State 进行编辑,将会得到错误。

    snap.enter {
        data.value = "Buzz"  // << --- Error
    }

在 snapshot 中,State 是唯读无法编辑的。若希望编辑,我们会需要 MutableSnapshot

fun main() {
    val data = mutableStateOf("")
    
    data.value = "Foo"
    
    // 1.
    val mutableSnap = Snapshot.takeMutableSnapshot()
    
    // 2.
    mutableSnap.enter {
        println("2: ${data.value}")
        data.value = "Buzz"
        println("3: ${data.value}")
    }
    
    
    println("4: ${data.value}")
    
    // 5.
    mutableSnap.enter {
        println("5: ${data.value}")
    }
    
    // 6.
    mutableSnap.apply()
    println("6: ${data.value}")
    
    mutableSnap.dispose()
}
Output:

2: Foo
3: Buzz
4: Foo
5: Buzz
6: Buzz
  1. 我们使用 takeMutableSnapshot 来建立 MutableSnapshot
  2. MutableSnapshot 中,我们一样可以拿到 takeSnapshot 时的资料 "Foo"
  3. MutableSnapshot 中,我们可以对 Sate 进行编辑
  4. 但这个编辑不会影响到外部的 State
  5. 再次进入 snapshot,可以再次拿到曾经在 snapshot 中编辑的 State,就样平行时空一样。
  6. 透过 MutableSnapshot.apply() 我们可以将 snapshot 的值实际赋予到 State 身上
  7. 注意,如果再 takeMutableSnapshotapply 之间,有人直接对 State set value,apply 也不会发生作用

监听 takeMutableSnapshot

fun takeMutableSnapshot(
    readObserver: ((Any) -> Unit)? = null,
    writeObserver: ((Any) -> Unit)? = null
): MutableSnapshot =
    (currentSnapshot() as? MutableSnapshot)?.takeNestedMutableSnapshot(
        readObserver,
        writeObserver
    ) ?: error("Cannot create a mutable snapshot of an read-only snapshot")

其实仔细看看 takeMutableSnapshot 的 signature,会发现其实他还有吃两个参数:read/write 的 Observer。
实际上我们可以透过这两个角色去监听到资料被写入、被读取的时机,并且取得即将被异动的 State 与其资料。

fun main() {
    val data = mutableStateOf("")
    
    data.value = "Foo"
    
    val readObserver = { readState: Any -> if (readState == data) println("READ")}
    
    val writeObserver = { readState: Any -> if (readState == data) println("WRITE")}
    
    val mutableSnap = Snapshot.takeMutableSnapshot(readObserver, writeObserver)
    
    mutableSnap.enter {
        println("1: ${data.value}")
        data.value = "Buzz"
        println("2: ${data.value}")
    }
    
    println("3: ${data.value}")
    
    mutableSnap.apply()
    println("4: ${data.value}")
    
    mutableSnap.dispose()
}
READ
1: Foo
WRITE
READ
2: Buzz
3: Foo
4: Buzz

println 前,我们需要去读取 data.value 并将资料喂给 println function ,因此每次在 snapshot 内的 println 之前都会触发 READ

回头说说 Recomposition

Composable 最後会被丢到 Recomposer 去执行,在 Recomposer 中我们可以发现这段 code

private inline fun <T> composing(
    composition: ControlledComposition,
    modifiedValues: IdentityArraySet<Any>?,
    block: () -> T
): T {
    val snapshot = Snapshot.takeMutableSnapshot(
        readObserverOf(composition), writeObserverOf(composition, modifiedValues)
    )
    try {
        return snapshot.enter(block)
    } finally {
        applyAndCheck(snapshot)
    }
}

private fun readObserverOf(composition: ControlledComposition): (Any) -> Unit {
    return { value -> composition.recordReadOf(value) }
}


// 

override fun recordReadOf(value: Any) {
    if (!areChildrenComposing) {
        composer.currentRecomposeScope?.let {    // <--- 
            it.used = true
            observations.add(value, it)
            
            ... 
        }
    }
}

source

Recomposer 在 composing 时,利用 snapshot 的 read/write observer 关注在 snapshot 中的 state 变化。并且在 read observer 时建立 recompose scope。

private fun writeObserverOf(
    composition: ControlledComposition,
    modifiedValues: IdentityArraySet<Any>?
): (Any) -> Unit {
    return { value ->
        composition.recordWriteOf(value)
        modifiedValues?.add(value)
    }
}


override fun recordWriteOf(value: Any) = synchronized(lock) {
    // ...
    derivedStates.forEachScopeOf(value) { // <--- 
        invalidateScopeOfLocked(it)
    }
}

在资料变动时,会 iterate 各个 scope 并且将他们 invalidate。


今天聊到的东西非常的底层,因为篇幅的关系,有些逻辑没有说明到。非常推荐大家到下面 Reference 的地方阅读本篇主要参考的文章。看了这些之後感觉对整个 compose 运作流程又再更近一步的认识了!


Reference:


<<:  D24 - 走!去浏览器偷听 Capturing & Bubbling

>>:  Vue.js指令介绍&基本指令(Directives)(DAY26)

[Day 20] Sass - Using @extend

哈罗~今天来聊聊跟@mixin的兄弟 @extend 我们平常在写css时,时常会把class们相同...

Day 3 - HTAP

上一篇提到了TiDB的特色之一,便是实践了HTAP。那HTAP又是什麽东西? HTAP全名Hybri...

统一状态管理 + 单一资料流

我一开始在学 Vuex 的时候,觉得很难懂,不知道它是在做什麽的。当时的我,就想先追朔单一资料流的始...

Android一键更换主题套件

前言 前阵子看了bilibili上的一些技术相关影片,码牛学院的公开课程 Android动态加载技术...

Day 06 | 资料绑定(二) - 计算属性 Computed

延续昨天的内容,在昨天理解完 mount() 後,今天就开始来对资料进行操作吧!! 资料绑定 如果用...