浅析状态管理库 - mobx的原理以及 仿写自己的状态管理库

一、简介

mobx 是一个非常优雅的状态管理库,具有相当大的自由度,并且使用非常简单。
另一方面,太自由有时候会导致滥用,或者使用不当,导致行为不符合自己的预期,比如我一开始在使用的时候就有困惑如下的:

  • action 到底有什么用?
  • autorun 怎么知道我使用了 observable 数据的?
  • 这个 autorun 的行为怎么如此怪,不符合预期,不是说声明了的值改变了就会自动执行?
  • ……

二、分析

首先还是丢出 github 的地址:

https://github.com/mobxjs/mobx


1、action 的作用

这个问题的关键就在 core/action 目录下
我们用 action 装饰了之后,执行的方法被这么包装


startAction 的代码如下


spy 是什么呢?看下官方说明,简而言之,spy 是一个全局的监控,监控每一个 action 行为,并统筹全局的 state 变更的状态,避免在一个 action 多次变更同一个 state 导致的多次 reaction 行为被调用,影响性能,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { observable } from 'mobx'
class Store {
@observable a = 0

test() {
this.a = 1
this.a = 2
this.a = 3
}
}

const store = new Store()

autorun(() => {
console.log(a)
})

store.test()
// 0
// 1
// 2
// 3

可以看到 autorun 除了初始化时执行了一次之外在每一次变更都被执行了一次
如果我们给 test 加上 action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { observable, action } from 'mobx'
class Store {
@observable a = 0

@action
test() {
this.a = 1
this.a = 2
this.a = 3
}
}


const store = new Store()

autorun(() => {
console.log(a)
})

store.test()
// 0
// 3

可以看到在一次加了 action 之后,在一次 action 中不会多次调用 autorun,更符合我们的预期行为(看需求),同时性能得到提升

PS:在 react 中,同步操作的视图更新会合并成一个事件,所以有没有加 action 在视图更新层面来说都是一次,但 Reaction 类的行为会多次执行

如果你看了 mobx 的代码,可以看到 mobx 的代码中充满了
if (notifySpy && process.env.NODE_ENV !== "production")
这也是 spy 的另外一个作用,就是帮助我们 debug,借助 mobx-react-devtools,我们可以清晰的看到数据变动,但是由于 mobx 太自由的写法,有些项目到处都是修改 state 的入口,会导致这个功能形同虚设 😂

2、mobx 的执行时收集依赖

mobx 还有一个很唬的能力就是执行时的依赖收集,他能知道你在 autorun,computed 中使用了哪些数据,并在数据变动后触发执行。
如果是刚开始接触,就会觉得不可思议,mobx 明明是一个运行时使用的数据管理的库,他又和我编写时没有关系,为什么会知道我的函数里使用了哪些变量呢?但仔细想想,他的这些监控都需要我们先运行一遍函数才行,可能是在这个地方动了手脚,翻开代码 core/reaction

首尾有两个可疑的方法
startBatch() endBatch()
看下这俩的代码

可以初步判断 mobx 是通过 globalState.inBatch 来标记依赖收集的开始和结束,
接下来看下 trackDerivedFunction

可以看到这一步主要是修改全局的状态,实际执行实际执行收集依赖的动作应该不在这个方法,应该和 observableValue 的 get 有关系

看下 reportObserved 方法

可以看到当前的 observable 被存进 derivation 中,自身也被标记为 isBeingObserved。
至此我们可以知道,我们可以回答后面的两个问题:

  • mobx 如何收集依赖?
    当 mobx 开始收集依赖时,会先标记一个收集状态,然后在执行包含需要被观测的 observableValue 的数据的方法,在 observableValue 的 get 方法中执行收集,最后再把收集状态关闭。

  • 为什么 autorun 的行为不符合预期?
    autorun 收集的依赖是在运行时可以被访问到的 observableValue 所以如下的用法是使用不当:

1
2
3
4
5
6
7
autorun(() => {
if (...) {
// ...
} else {
// ...
}
})

被监控到的值是可以被访问到的数据,所以必定只会对 if 中中或者 else 中的变化作出反应,还有一个就是加了@action 之后一次 action 只会执行一次 autorun(可能就不想预期一样可以监控每一次变化)

二、仿写

1、Derivation

这个类相当于一个依赖收集器,负责收集 observable 对应 reaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
const trackWatches = [] // 存放reaction的栈,处理嵌套

class Derivation {

constructor() {
this.mEvents = new Map() // observable映射到的reaction
this.reactionMap = new WeakMap() // reaction映射到的observable
this.collecting = false // 是否在收集依赖
this.reId = null // reaction的Id
}

beginCollect(reaction) {
this.collecting = true
if (reaction) {
trackWatches.push(reaction)
this.currentReaction = reaction
this.reId = reaction.id
}
}

endCollect() {
trackWatches.pop()
this.currentReaction = trackWatches.length ? trackWatches[trackWatches.length - 1] : null
this.currentReaction ? this.reId = this.collectReaction.id : null
if (!this.currentReaction) {
this.collecting = false
this.reId = null
}
}

collect(id) {
if (this.collecting) {
// 收集reaction映射到的observable
const r = this.reactionMap.get(this.currentReaction)
if (r && !r.includes(id)) r.push(id)
else if (!r) this.reactionMap.set(this.currentReaction, [id])

// 收集observable映射到的reaction
const mEvent = this.mEvents.get(id)
if (mEvent && !mEvent.watches.some(reaction => reaction.id === this.reId)) {
mEvent.watches.push(this.currentReaction)
} else {
this.mEvents.set(id, {
watches: [this.currentReaction]
})
}
}
}

fire(id) {
const mEvent = this.mEvents.get(id)
if (mEvent) {
mEvent.watches.forEach((reaction) => reaction.runReaction())
}
}

drop(reaction) {
const relatedObs = this.reactionMap.get(reaction)
if (relatedObs) {
relatedObs.forEach((obId) => {
const mEvent = this.mEvents.get(obId)
if (mEvent) {
let idx = -1
if ((idx = mEvent.watches.findIndex(r => r === reaction)) > -1) {
mEvent.watches.splice(idx, 1)
}
}
})
this.reactionMap.delete(reaction)
}
}
}

const derivation = new Derivation()
export default derivation

这里简单实现,把所有回调行为都当作是一个 reaction,相当于一个 eventBus 但是,key 是 obId,value 就是 reaction,只是省去了注册事件的步骤

2、Observable

首先实现 observable,这里因为主要是以实现功能为主,不详细(只监控原始类型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import derivation from './m-derivation'

let OBCount = 1
let OB_KEY = Symbol()

class Observable {

constructor(val) {
this.value = val
this[OB_KEY] = `ob-${OBCount++}`
}

get() { // 在开启收集依赖时会被derivation收集
derivation.collect(this[OB_KEY])
return this.value
}

set(value) { // 设置值时触发
this.value = value
derivation.fire(this[OB_KEY])
return this.value
}
}

export default Observable

根据 Observable 简单封装一下,监控原始数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19


// 暴露的接口
import Observable from '../core/m-observable'

const PRIMITIVE_KEY = 'value'
export const observePrimitive = function(value) {
const data = new Observable(value)
return new Proxy(data, {
get(target, key) {
if (key === PRIMITIVE_KEY) return target.get()
return Reflect.get(target, key)
},
set(target, key, value, receiver) {
if (key === PRIMITIVE_KEY) return target.set(value)
return Reflect.set(target, key, value, receiver) && value
}
})
}

3、Reaction

实际被调用的一方,当 observable 的数据发生变化时会通过 Derivation 调用相应的 reaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import derivation from './m-derivation'

let reId = 0

class Reaction {
constructor(obCollect, handle, target) {
this.id = `re-${reId++}`
this.obCollect = obCollect
this.reactHandle = handle
this.target = target
this.disposed = false // 是否不再追踪变化
}

track() {
if (!this.disposed) {
derivation.beginCollect(this, this.reactHandle)
const value = this.obCollect()
derivation.endCollect()
return value
}
}

runReaction() {
this.reactHandle.call(this.target)
}

dispose() {
if (!this.disposed) {
this.disposed = true
derivation.beginCollect()
derivation.drop(this)
derivation.endCollect()
}
}
}

export default Reaction

再把 Reaction 封装一下,暴露出 autorun 和 reaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import Reaction from '../core/m-reaction'

export const autorun = function(handle) {
const r = new Reaction(handle, handle)
r.track()
return r.dispose.bind(r)
}

export const reaction = function(getObData, handle) {
let prevVal = null // 数据变化时调用
const wrapHandle = function() {
if (prevVal !== (prevVal = getObData())) {
handle()
}
}

const r = new Reaction(getObData, wrapHandle)
prevVal = r.track()
return r.dispose.bind(r)
}

4、测试 autorun 和 reaction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { observePrimitive, autorun, reaction } from './m-mobx'

class Test {

constructor() {
this.a = observePrimitive(0)
}

increase() {
this.a.value++
}
}

const test = new Test()

autorun(() => {
console.log('@autorun a:', test.a.value)
})

window.dis = reaction(() => test.a.value,
() => {
console.log('@reaction a:', test.a.value)
})

window.test = test

5、Computed

computed 类型数据乍看之下和 get 没有什么不同,但 computed 的特殊之处在于他即是观察者同时又是被观察者,所以我也把它当成一个 reaction 来实现,mobx 的 computed 还提供了一个 observe 的钩子,其内部实现其实也是一个 autorun

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import derivation from './m-derivation'
import { autorun } from '../m-mobx'

/**
* observing observed
*/

let cpId = 0

class ComputeValue {
constructor(options) {
this.id = `cp-${cpId++}`
this.options = options
this.value = options.get()

}

get() { // 收集cp的依赖
derivation.collect(this.id)
return this.value
}

computedValue() { // 收集ob依赖
this.value = this.options.get()
return this.value
}

track() { // 收集ob
derivation.beginCollect(this)
this.computedValue()
derivation.endCollect()
}

observe(fn) {
if (!fn) return
let prevValue = null
let firstTime = true
autorun(() => {
const newValue = this.computedValue()
if (!firstTime) {
fn({ prevValue, newValue })
}
prevValue = newValue
firstTime = false
})
}

runReaction() {
this.computedValue()
derivation.fire(this.id)
}
}

export default ComputeValue

所以他的流程是这样的:

  • 在调用 computed 的时候先收集 observaleValue 对应的 computedValue
  • 在 computed.observe 的时候则是直接收集 observableValue 对应 reaction
  • 在 autorun 中收集 computed 依赖实际上手机的事 computedValue 对应的 observableValue

6、测试 computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { observePrimitive, autorun, reaction, computed } from './m-mobx'

class Test {

constructor() {
this.a = observePrimitive(0)
this.b = computed(() => {
return this.a.value + 10
})
this.b.observe((change) => console.log('@computed b:', change.prevValue, change.newValue))
}

increase() {
this.a.value++
}
}

const test = new Test()

reaction(() => {
console.log('@reaction a:', test.a.value)
})

autorun(() => {
console.log('@autorun b:', test.b.get())
})

window.test = test

三、总结

了解 action,autorun,computed 做了什么,并自己简单实现了一个数据管理的库,加深了我对 mobx 的理解,并直接催生了本文的诞生。(对于我后续使用 mobx 这个库有相当大的帮助(至少不会滥用了)😂)
希望大家看完本文后有所收获,对大家后续的学习和工作有所帮助。


如果发现本文有任何错误,欢迎直接指出,交流学习 😊