A list,

Mobx is a very elegant state management library with considerable freedom and is very simple to use. On the other hand, too much freedom will sometimes lead to abuse or improper use, resulting in behavior that does not meet our expectations. For example, I was confused as follows at the beginning of using:

  • What exactly is the use of action?
  • How does Autorun know I use Observable data?
  • This autorun behavior is so strange, does not conform to the expected, does not say that the declared value changes will automatically execute?

Second, the analysis

Github address first:

github.com/mobxjs/mobx


1. Action

The key to this problem is that in the core/ Action directory, after we’ve wrapped up the action, the method we’re going to execute is wrapped this way

The official instructions

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/3Copy the code

You can see that Autorun is executed every time it changes except once during initialization if we add action to test

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 / / 3Copy the code

As you can see, after an action is added, autorun is not called many times in an action, which is more consistent with our expected behavior (see requirements), and performance is improved

PS: In React, simultaneous view updates are consolidated into one event, so whether an action is added or not is a one-time view update. Reaction actions are executed multiple times

If you look at mobx code, you can see that mobx code is full of if (notifySpy && process.env.node_env! == “production”) this is another function of SPY, which helps us debug. With mobx-react-devtools, we can clearly see data changes, but because mobx is too free to write, some projects are full of entry to modify state. Can lead to this function in vain 😂

Mobx collects dependencies at execution time

Mobx also has the ability to rely on collection at runtime, which tells you what data you are using in Autorun, computed, and triggers execution if the data changes. Mobx is a runtime data management library, and it has nothing to do with when I write it. Why does mobx know what variables are used in my functions? But if you think about it, all of his monitoring requires us to run the function first, which is probably where we’re going to do something, core/ Reaction

startBatch() endBatch()

  • How does Mobx collect dependencies? When MOBx starts collecting dependencies, it marks a collection state, executes a method that contains the data to be observed for the observableValue, executes the collection in the observableValue’s GET method, and turns the collection state off.

  • Why is Autorun not behaving as expected? The autorun collection relies on observableValue that can be accessed at run time, so the following usage is inappropriate:

autorun(() => {
    if (...) {
        // ...
    } else{/ /... }})Copy the code

The values monitored are data that can be accessed, so it must only respond to changes in the if or else. Another is that an action will only execute autorun once after @action is added.

Second, the copy writing

1, the Derivation

This class acts as a dependency collector that collects Observable reactions

Const trackWatches = []constructor() {this.mevents = new Map() // Reaction to Observable this.reaction = new WeakMap() // Reaction to Observable this.collecting =falseReId = 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) {// Collect reaction map to Observable const r = this.get (this.currentreAction)if(r && ! r.includes(id)) r.push(id)else if(! Reaction const mEvent = this.mevents. Get (id) Reaction const mEvent = this.mevents.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
Copy the code

The simple implementation here is to treat all callbacks as a reaction, which is equivalent to an eventBus, but the key is an obId and the value is the reaction, eliminating the need to register the event

2, observables

Observable first implements an Observable, which is not detailed (just monitoring primitive types).

import derivation from './m-derivation'

let OBCount = 1
let OB_KEY = Symbol()

class Observable {

  constructor(val) {
    this.value = val
    this[OB_KEY] = `ob-${OBCount++}`}getCollect (this[OB_KEY]) {// Collect variable.collect (this[OB_KEY])return this.value
  }

  set(value) {// Trigger this.value = value od.fire (this[OB_KEY])return this.value
  }
}

export default Observable
Copy the code

Simple Observable encapsulation monitors raw data types

// The exposed interface 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
    }
   })
}
Copy the code

3, Reaction

The actual called party invokes reaction through Derivation when the data of the Observable changes

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// Stop tracking changes}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
Copy the code

Encapsulate the Reaction again, exposing autorun and 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) {
  letPrevVal = null // Call const wrapHandle = when data changesfunction() {
    if(prevVal ! == (prevVal = getObData())) { handle() } } const r = new Reaction(getObData, wrapHandle) prevVal = r.track()return r.dispose.bind(r)
}
Copy the code

4. Test autorun and 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
Copy the code

5, the Computed

Computed data is not different from GET at first glance, but computed is special because it is both the observer and the observed, so I also regard it as a reaction. Mobx computed also provides an observe hook. Its internal implementation is also an 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() {// Collect the dependencies of CP.collect (this.id)return this.value
  }

  computedValue() {// Collect ob dependency this.value = this.options.get()return this.value
  }

  track() {/ / collect ob derivation. BeginCollect (this) this.com putedValue () derivation. EndCollect ()} to 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
Copy the code

So his process went like this:

  • Collect the computedValue corresponding to observaleValue before invoking computed
  • In computed. Observe, the observableValue is directly collected for reaction
  • Collecting computedValue in Autorun relies on the observableValue corresponding to the actual mobile phone

6. Test 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
Copy the code

Third, summary

Understanding what Action, Autorun, computed do and simply implementing a data management library on my own deepened my understanding of Mobx and led directly to this article. (For my follow-up use of the Mobx library has a considerable help (at least not abuse) 😂) I hope that you have learned something after reading this article, it will be helpful for your follow-up study and work.


If you find any mistakes in this article, please point them out directly at 😊