浅析mobx原理并 仿写自己的数据管理库
栏目: JavaScript · 发布时间: 5年前
内容简介:mobx 是一个非常优雅的状态管理库,具有相当大的自由度,并且使用非常简单。 另一方面,太自由有时候会导致滥用,或者使用不当,导致行为不符合自己的预期,比如我一开始在使用的时候就有困惑如下的:首先还是丢出github的地址:
mobx 是一个非常优雅的状态管理库,具有相当大的自由度,并且使用非常简单。 另一方面,太自由有时候会导致滥用,或者使用不当,导致行为不符合自己的预期,比如我一开始在使用的时候就有困惑如下的:
- action到底有什么用?
- autorun怎么知道我使用了observable数据的?
- 这个autorun的行为怎么如此怪,不符合预期,不是说声明了的值改变了就会自动执行?
- ……
二、分析
首先还是丢出github的地址:
1、action的作用
这个问题的关键就在core/action目录下 我们用action装饰了之后,执行的方法被这么包装
startAction的代码如下
spy是什么呢?看下 官方说明,简而言之,spy是一个全局的监控,监控每一个action行为,并统筹全局的state变更的状态,避免在一个action多次变更同一个state导致的多次reaction行为被调用,影响性能,举个例子:
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
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的入口,会导致这个功能形同虚设:joy:
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所以如下的用法是使用不当:
autorun(() => { if (...) { // ... } else { // ... } }) 复制代码
被监控到的值是可以被访问到的数据,所以必定只会对if中中或者else中的变化作出反应,还有一个就是加了@action之后一次action只会执行一次autorun(可能就不想预期一样可以监控每一次变化)
二、仿写
1、Derivation
这个类相当于一个依赖收集器,负责收集observable对应reaction
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,这里因为主要是以实现功能为主,不详细(只监控原始类型)
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简单封装一下,监控原始数据类型
// 暴露的接口 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
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
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
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
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
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 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。