浅析状态管理库 - mobx的原理以及 仿写本身的状态管理库

1、简介

mobx 是一个很是优雅的状态管理库,具备至关大的自由度,而且使用很是简单。 另外一方面,太自由有时候会致使滥用,或者使用不当,致使行为不符合本身的预期,好比我一开始在使用的时候就有困惑以下的:html

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

2、分析

首先仍是丢出github的地址:react

github.com/mobxjs/mobxgit


一、action的做用

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

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加上actionbash

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,更符合咱们的预期行为(看需求),同时性能获得提高ide

PS:在react中,同步操做的视图更新会合并成一个事件,因此有没有加action在视图更新层面来讲都是一次,但Reaction类的行为会屡次执行函数

若是你看了mobx的代码,能够看到mobx的代码中充满了 if (notifySpy && process.env.NODE_ENV !== "production") 这也是spy的另一个做用,就是帮助咱们debug,借助mobx-react-devtools,咱们能够清晰的看到数据变更,可是因为mobx太自由的写法,有些项目处处都是修改state的入口,会致使这个功能形同虚设😂性能

二、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(可能就不想预期同样能够监控每一次变化)

2、仿写

一、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,只是省去了注册事件的步骤

二、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
    }
   })
}
复制代码

三、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)
}
复制代码

四、测试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
复制代码

五、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

六、测试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
复制代码

3、总结

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


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

相关文章
相关标签/搜索