淺析狀態管理庫 - 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這個庫有至關大的幫助(至少不會濫用了)😂) 但願你們看完本文後有所收穫,對你們後續的學習和工做有所幫助。


若是發現本文有任何錯誤,歡迎直接指出,交流學習😊

相關文章
相關標籤/搜索