About source code Interpretation

Welcome to star or fork my source code for the mobx series!

Intermittent took some time, just put the whole context clear, and for some details to do the annotation, the first interpretation of a more complex source code, many places refer to the description of others, because I think the description is clearer than their own.

Look at the source is the front end of the way to advance, first of all can know the principle, when writing business code can be at ease, and can expand the higher order function; Secondly, understand the principle can help you quickly eliminate obstacles and avoid the emergence of bugs; Finally, reading source code can learn excellent programming paradigms, so that their own programming thinking and habits have subtle changes, this change is the most important.

The mobx version is 5.15.4. I removed the old code of V4 from the source code and explained it based on V5.

The main concept

Observable Observable

In Mobx, we need to trigger an action or response when a value or object is changed. This pattern is typical of the observer mode (or publisk-subscribe mode), where a value or object is the observed and the action or response acts as the observer.

The core idea is easy to understand. First, you do a proxy (or defineProperty) so that an object becomes an Observable. Secondly, when observers execute principal logic, they will access proxy object properties. In this case, the proxy object actively reports itself to observing, and simultaneously places observers in observers queue of Observable. The observer and the observed have references to each other and the relationship is formally established. Finally, when the proxy object properties are set, the proxy object reportChanged the observer to execute the principal logic.

It may be hard to figure out how MOBx works in words, so the code and call link will explain it in detail.

Read the source code, first of all to clear the basic use of the library and the meaning of the interface, look at the source code to not be confused, if there is a working principle of the call link guide, it is equivalent to paving the way, since the peak.

1. How to establish a relationship between observer and observed

In MOBX, observers have reaction and Autorun. Autorun is a special reaction, and reaction implements self-derivation, that is to say, derivation is the fundamental observer. The observed object is an Observable.

** Note: Both observable variables and the observed are referred to as’ Observable ‘**

Call link

(1Observable collects observer reaction =newReaction() --> reaction.track() --> trackDerivedFunction() --> bindDependencies(derivation) --> addObserver(observable, Derivation) -- - > observables. Observers. The add (node) (2Observablevalue.get () --> ObservableValue.observableValue.get () -->this.reportObserved(observable) --> derivation.newObserving! [derivation. UnboundDepsCount++] = observables (3Observablevalue.set () -->this.reportChanged(observable) -->  propagateChanged(this) --> observable.observers.forEach((d) = >{d.o nBecomeStale ()}) - > d.s chedule () - > globalState. PendingReactions. Push (d) and runReactions () - > The derivation of the dilemma between the dilemma and the implementation of the dilemmathis.oninvalidate () is user-defined logicCopy the code

2. Core logic explanation

Reaction

At the heart of the Reaction class is the track method, which starts a transaction in which trackDerivedFunction() is called to perform a user-defined logical FN () and relational binding.

// Truncated code
track(fn: (a)= > void) {
        startBatch()
  
        const result = trackDerivedFunction(this, fn, undefined)
     
        if (this.isDisposed) {
            // disposed during last run. Clean up everything that was bound after the dispose call.
            clearObserving(this)
        }
        
        endBatch()
    }
Copy the code
derivation

TrackDerivedFunction () is defined in Derivation.

function trackDerivedFunction<T> (derivation: IDerivation, f: () = >T.context: any) {
    const prevAllowStateReads = allowStateReadsStart(true)
    // pre allocate array allocation + room for variation in deps
    // array will be trimmed by bindDependencies
    / / the derivation. DependenciesState and derivation. Observing all ob within an array. LowestObserverState IDerivationState instead. UP_TO_DATE (0)
    / / call changeDependenciesStateTo0 method will first derivation and observing for steady-state UP_TO_DATE, mainly convenience is in the phase of collecting rely on follow-up
    changeDependenciesStateTo0(derivation)
    // Apply space for new observing in advance, and trim it later
    derivation.newObserving = new Array(derivation.observing.length + 100)
    // unboundDepsCount records the number of unbound observables that an Observable updates with reportObserved() when observed by an observer
    derivation.unboundDepsCount = 0
    derivation.runId = ++globalState.runId
    / / save Reaction context, will be the Reaction of the current assignment to globalState. TrackingDerivation for bindDependencies rely on collection
    const prevTracking = globalState.trackingDerivation
    globalState.trackingDerivation = derivation
    let result
    if (globalState.disableErrorBoundaries === true) {
        result = f.call(context)
    } else {
        try {
            // This step triggers observable access (since f accesses observable properties),
            / / that we ob. Name -- - > $mobx. Name. The get () ObservableValue. Prototype. (get)
            // -->reportObserved(ObservableValue)
            
            // Call the track function, which in mox-react is the component's render method
            result = f.call(context)
        } catch (e) {
            result = new CaughtException(e)
        }
    }
    // Restore the Reaction context
    globalState.trackingDerivation = prevTracking
    //Reaction establishes a relationship with Observable
    bindDependencies(derivation)

    warnAboutDerivationWithoutDependencies(derivation)

    allowStateReadsEnd(prevAllowStateReads)

    return result
}
Copy the code

Result = f.call(context) triggers observable.get(), Observable observable observable observable observable observable observable observable observable observable observable observable observable observable observable observable observable

Next, establish relationship between Reaction and Observable bindDependencies(Derivation).

function bindDependencies(derivation: IDerivation) {
    // invariant(derivation.dependenciesState ! == IDerivationState.NOT_TRACKING, "INTERNAL ERROR bindDependencies expects derivation.dependenciesState ! = = 1 ");
    // Temporarily store the old Observable list
    const prevObserving = derivation.observing
    // Replace the old List with the new Observable list
    const observing = (derivation.observing = derivation.newObserving!)
    let lowestNewObservingDerivationState = IDerivationState.UP_TO_DATE

    // Go through all new observables and check diffValue: (this list can contain duplicates):
    // 0: first occurrence, change to 1 and keep it
    // 1: extra occurrence, drop it
    // Iterate over all new Observables, removing duplicate observables
    let i0 = 0,
        l = derivation.unboundDepsCount
    for (let i = 0; i < l; i++) {
        // I is a fast pointer, i0 is a slow pointer, I is a fast pointer
        const dep = observing[i]
        // Skip duplicate values, i.e. DiffValue = 1; I is not equal to i0 when duplicate values are skipped, I is ahead of i0
        if (dep.diffValue === 0) {
            dep.diffValue = 1
            if(i0 ! == i) observing[i0] = dep i0++ }// Upcast is 'safe' here, because if dep is IObservable, `dependenciesState` will be undefined,
        // not hitting the condition
        if (((dep as any) as IDerivation).dependenciesState > lowestNewObservingDerivationState) {
            lowestNewObservingDerivationState = ((dep as any) as IDerivation).dependenciesState
        }
    }
    observing.length = i0

    derivation.newObserving = null // newObserving shouldn't be needed outside tracking (statement moved down to work around FF bug, see #614)

    // Go through all old observables and check diffValue: (it is unique after last bindDependencies)
    // 0: it's not in new observables, unobserve it
    // 1: it keeps being observed, don't want to notify it. change to 0
    // Walk through the old Observable list:
    // diffValue 0 indicates that diffValue is not in the new Observable list (The diffValue of each new observables is set to 1).
    // diffValue 1 indicates that the value is still being observed. (Each round of dependency updates, if an observable DEP was also in the dependency list in the previous round,
    // In this case, the DEP object is the same, and the diffValue will be updated to 1 when a new round updates the newObserving dependency.
    // As clever as newObserving, diffValue is very useful
    l = prevObserving.length
    while (l--) {
        const dep = prevObserving[l]
        if (dep.diffValue === 0) {
            removeObserver(dep, derivation)
        }
        // Set diffValue to 0 after the old and new iteration, i.e. First occurrence above
        dep.diffValue = 0
    }

    // Go through all new observables and check diffValue: (now it should be unique)
    // 0: it was set to 0 in last loop. don't need to do anything.
    // 1: it wasn't observed, let's observe it. set back to 0
    // this operation is required because the first step of newObserving filtering is the newObserving object.
    // prevObserving sets the diffValue of the dependency to 0, but the prevObserving dependency already belongs to addObserver().
    // If the diffValue of newObserving is set to 0, then the addObserver() will be executed.
    while (i0--) {
        const dep = observing[i0]
        if (dep.diffValue === 1) {
            dep.diffValue = 0
            // Register an observer for observableValue
            Observable (object, array, set...) // Change observable(object, array, set...) Call this.atom.reportChanged() to send notifications
            // Foreach notifies each reaction by calling onBecomeStale, also known as the Schedule method

            // Call link: value change --> Observable set(newVal) --> this.atom.reportchanged ()
            / / -- > propagateChanged (this) - > observables. Observers. Call forEach observer. OnBecomeStale ()
            / / -- > reaction. The schedule () - > globalState. PendingReactions. Push (this) and runReactions ()
            // --> reactionScheduler() 
            // --> allReactions.foreach () execute each reaction reaction. RunReaction ()
            // --> execute each reaction this.oninvalidate ()
            addObserver(dep, derivation)
        }
    }
    // NOTE:The collected dependencies are saved in Reaction. Observing, which is called in the getDependencyTree API

    / / for the newly added observation data, add derivation globalState. PendingReactions,
    // In the current transaction cycle
    // Some new observed derivations may become stale during this derivation computation
    // so they have had no chance to propagate staleness (#916)
    if(lowestNewObservingDerivationState ! == IDerivationState.UP_TO_DATE) { derivation.dependenciesState = lowestNewObservingDerivationState derivation.onBecomeStale() } }Copy the code

After understanding derivation, another important knowledge point is the state, which represents the different stages of derivation and observable, through which the execution of logic can be better controlled. The derived or observable has four states, the higher the value, the more unstable it is:

NOT_TRACKING: Status when the observable is no longer subscribed initially or spawned

UP_TO_DATE: indicates that the current value is the latest or the derived dependency has not changed and does not need to be recalculated

POSSIBLY_STALE: state when the dependency of a calculated value changes, indicating that the calculated value may be changed. For example, computational value A relies on Observable B and Observable C. If both b and C change, but the final result, A, does not change, there is no need to notify observers of A of their execution logic

STALE: Indicates that the derived logic needs to be executed again. In this case, the derived dependent object is changed

enum IDerivationState {
    NOT_TRACKING = - 1,
    UP_TO_DATE = 0,
    POSSIBLY_STALE = 1,
    STALE = 2
}
Copy the code
observable

Objects become observable after mobx processing, either through a proxy or defineProperty proxy. In MOBx, a value of a basic type can be an Observable, and an array /map/set/object can also be an Observable. However, there are some differences in their processing methods.

When we decorate a variable:

import { observable } from "mobx"

class Todo {

  id = Math.random()

  @observable title = ""

  @observable finished = false

}
Copy the code

Observable actually calls createObservable, which in turn calls observable methods.

So what does an Observable do? Look at its type.

const observable: IObservableFactory & IObservableFactories & { enhancer: IEnhancer<any>}
Copy the code

IObservableFactories is an interface, and observableFactories is an implementation of it, proxying and hijacking basic data types and objects.

const observableFactories: IObservableFactories = {
    // For basic types string, Boolean, number can be hijacked by boxbox<T = any>(value? : T, options? : CreateObservableOptions): IObservableValue<T> {if (arguments.length > 2) incorrectlyUsedAsDecorator("box")
        const o = asCreateObservableOptions(options)
        // getEnhancerFromOptions(o) generates enhancer
        return new ObservableValue(value, getEnhancerFromOptions(o), o.name, true, o.equals) }, array<T = any>(initialValues? : T[], options? : CreateObservableOptions): IObservableArray<T> {if (arguments.length > 2) incorrectlyUsedAsDecorator("array")
        const o = asCreateObservableOptions(options)
        return createObservableArray(initialValues, getEnhancerFromOptions(o), o.name) asany }, map<K = any, V = any>( initialValues? : IObservableMapInitialValues<K, V>, options? : CreateObservableOptions ): ObservableMap<K, V> {if (arguments.length > 2) incorrectlyUsedAsDecorator("map")
        const o = asCreateObservableOptions(options)
        return new ObservableMap<K, V>(initialValues, getEnhancerFromOptions(o), o.name)
    },
    set<T = any>(
        initialValues?: IObservableSetInitialValues<T>,
        options?: CreateObservableOptions
    ): ObservableSet<T> {
        if (arguments.length > 2) incorrectlyUsedAsDecorator("set")
        const o = asCreateObservableOptions(options)
        return new ObservableSet<T>(initialValues, getEnhancerFromOptions(o), o.name)
    },
    object<T = any>(
        props: T,
        decorators?: { [K in keyof T]: Function}, options? : CreateObservableOptions ): T & IObservableObject {if (typeof arguments[1= = ="string") incorrectlyUsedAsDecorator("object")
        const o = asCreateObservableOptions(options)
        if (o.proxy === false) {
            // Use object.defineProperty hijacking
            return extendObservable({}, props, decorators, o) as any
        } else {
            const defaultDecorator = getDefaultDecoratorFromObjectOptions(o)
            // extendObservable will assign adm to the property $mobx for proxy invocation
            const base = extendObservable({}, undefined.undefined, o) as any
            const proxy = createDynamicObservableObject(base)
            extendObservableObjectWithProperties(proxy, props, decorators, defaultDecorator)
            return proxy
        }
    },
    ref: refDecorator,
    shallow: shallowDecorator,
    deep: deepDecorator,
    struct: refStructDecorator
} as any
Copy the code

ObservableFactories define hijacking methods for data types. ObservableFactories define hijacking methods for data types. ObservableFactories define hijacking methods for data types.

Object.keys(observableFactories).forEach(name= > (observable[name] = observableFactories[name]))
Copy the code

You get the idea.

Back to Observable, the createObservable function is actually called:

Do not hijack incoming values or objects.

function createObservable(v: any, arg2? : any, arg3? : any) {
    // @observable someProp;
    if (typeof arguments[1= = ="string" || typeof arguments[1= = ="symbol") {
        return deepDecorator.apply(null.arguments as any)
    }

    // it is an observable already, done
    if (isObservable(v)) return v

    // something that can be converted and mutated?
    const res = isPlainObject(v)
        ? observable.object(v, arg2, arg3)
        : Array.isArray(v)
        ? observable.array(v, arg2)
        : isES6Map(v)
        ? observable.map(v, arg2)
        : isES6Set(v)
        ? observable.set(v, arg2)
        : v

    // this value could be converted to a new observable data structure, return it
    if(res ! == v)return res
}
Copy the code
The object of hijacked

The object function takes three arguments, the third of which is options to customize the hijacking method.

Example:

const person = observable({
    name: 'lawler',
    get labelText() {
        return this.showAge ? `The ${this.name} (age: The ${this.age}) ` : this.name; }, setAge(age) { his.age = age; }}, {// This is the second parameter decorators
    // setAge is set to action, and the other properties default to observables/computed
    setAge: action 

} /*, pass the third option *\/);Copy the code

Let’s see how it works:

object<T = any>( props: T, decorators? : { [Kin keyof T]: Function}, options? : CreateObservableOptions ): T & IObservableObject {if (typeof arguments[1= = ="string") incorrectlyUsedAsDecorator("object")
        const o = asCreateObservableOptions(options)
        if (o.proxy === false) {
            // Use object.defineProperty hijacking
            return extendObservable({}, props, decorators, o) as any
        } else {
            const defaultDecorator = getDefaultDecoratorFromObjectOptions(o)
            // extendObservable will assign adm to the property $mobx for proxy invocation
            const base = extendObservable({}, undefined.undefined, o) as any
            const proxy = createDynamicObservableObject(base)
            extendObservableObjectWithProperties(proxy, props, decorators, defaultDecorator)
            return proxy
        }
    
Copy the code

Step 1: Generate configuration options

If options is passed as a string, then

const o = { name: thing, deep: true.proxy: true }
Copy the code

If no configuration item is passed, the default configuration item is returned. Therefore, the default is proxy hijacking

const defaultCreateObservableOptions = {
    deep: true.name: undefined.defaultDecorator: undefined.proxy: true
}
Copy the code

Step 2: Generate the default decorator

constDefaultDecorator = getDefaultDecoratorFromObjectOptions deepDecorator (o) the default item returnedCopy the code

The default deepDecorator part of the code:

const deepDecorator = createDecoratorForEnhancer(deepEnhancer)

// Removed some development environment code
function createDecoratorForEnhancer(enhancer: IEnhancer<any>) :IObservableDecorator {
    invariant(enhancer)
    const decorator = createPropDecorator(
        true,
        (
            target: any,
            propertyName: PropertyKey,
            descriptor: BabelDescriptor | undefined,
            _decoratorTarget,
            decoratorArgs: any[]
        ) => {
            const initialValue = descriptor
                ? descriptor.initializer
                    ? descriptor.initializer.call(target)
                    : descriptor.value
                : undefined
            / * * * asObservableObject, its incoming parameters as the original object, * the return value is the adm * ObservableObjectAdministration type object at the same time the adm is bound to the $mobx attributes, The common object uses * * and chain-calls addObservableProp, * via enhancer, to assign the hijacked initialValue */ to the propertyName property
            asObservableObject(target).addObservableProp(propertyName, initialValue, enhancer)
        }
    )
   
    const res: any = decorator
    res.enhancer = enhancer
    return res
}
Copy the code

Focus on asObservableObject(Target).addobServableProp (propertyName, initialValue, enhancer).

The first is asObservableObject:

This method generates an ADM object and returns it, assigning the adm to the object’s $mobx property for the object to use.

export function asObservableObject(
    target: any,
    name: PropertyKey = "",
    defaultEnhancer: IEnhancer<any> = deepEnhancer
) :ObservableObjectAdministration {
    if (Object.prototype.hasOwnProperty.call(target, $mobx)) return target[$mobx]

    if(! isPlainObject(target)) name = (target.constructor.name ||"ObservableObject") + "@" + getNextId()
    if(! name) name ="ObservableObject@" + getNextId()

    const adm = new ObservableObjectAdministration(
        target,
        new Map(),
        stringifyKey(name),
        defaultEnhancer
    )
    addHiddenProp(target, $mobx, adm)
    return adm
}
Copy the code

While ObservableObjectAdministration encapsulates some read, write, and from the method

/** * adm is a wrapper around values, which is a Map with PropertyKey and ObservableValue. * the last provide API is called ObservableValue * / class ObservableObjectAdministration constructor (public target: any, public values = new Map<PropertyKey, ObservableValue<any> | ComputedValue<any>>(), public name: string, public defaultEnhancer: IEnhancer<any> ) { this.keysAtom = new Atom(name + ".keys") } read(key: PropertyKey) { return this.values.get(key)! .get()} write(key: PropertyKey, newValue) {// omit} has(key: PropertyKey) {// omit} addObservableProp(propName: PropertyKey, newValue, enhancer: IEnhancer<any> = this. DefaultEnhancer) {// omit} addComputedProp(propertyOwner: any, // where is the property declared? propName: PropertyKey, options: IComputedValueOptions<any>) {// omit} remove(key: PropertyKey) {// omit} observe(callback: (changes: IObjectDidChange) => void, fireImmediately?: boolean): Lambda { return registerListener(this, callback) } }Copy the code

Next call adm.addObservableProp(propertyName, initialValue, enhancer) :

Add attributes to the Observable and hijack the set/get operation. InitialValue is also changed to ObservableValue, and finally the property name key value is stored in the adm. Values object (used in the actual proxy proxy, see below).

 addObservableProp(
        propName: PropertyKey,
        newValue,
        enhancer: IEnhancer<any> = this.defaultEnhancer
    ) {
        / / this is adm
        const { target } = this
        assertPropertyConfigurable(target, propName)

        const observable = new ObservableValue(
            newValue,
            enhancer,
            `The ${this.name}.${stringifyKey(propName)}`.false
        )
        
        this.values.set(propName, observable)
        newValue = (observable as any).value // observableValue might have changed it

        Object.defineProperty(target, propName, generateObservablePropConfig(propName))
        this.notifyPropertyAddition(propName, newValue)
    }
Copy the code

Step 3: The Base object is an empty object, but the attribute $mobx value is an ADM object

const base = extendObservable({}, undefined.undefined, o) as any
Copy the code
function extendObservable<A extends Object.B extends Object> (target: A, properties? : B, decorators? : { [K in keyof B]? : Function }, options? : CreateObservableOptions) :A & B {
    options = asCreateObservableOptions(options)
    const defaultDecorator = getDefaultDecoratorFromObjectOptions(options)
    // Target is empty object '{}'
    initializeInstance(target) 
    // Target's attribute $mobx is the adm object
    asObservableObject(target, options.name, defaultDecorator.enhancer) // make sure object is observable, even without initial props
    if (properties)
        extendObservableObjectWithProperties(target, properties, decorators, defaultDecorator)
    return target as any
}
Copy the code

Step 4: Proxy objects

const proxy = createDynamicObservableObject(base)
Copy the code
function createDynamicObservableObject(base) {
    // Agent traps define agent properties (has, get, set, etc.) that call methods on adm objects
    const proxy = new Proxy(base, objectProxyTraps)
    base[$mobx].proxy = proxy
    return proxy
}
Copy the code

In the Get method of The objectProxyTraps, an Observable is retrieved from adm.values.get(name) for use.

 get(target: IIsObservableObject, name: PropertyKey) {
        if (name === $mobx || name === "constructor" || name === mobxDidRunLazyInitializersSymbol)
            return target[name]
        const adm = getAdm(target)
        const observable = adm.values.get(name)
        if (observable instanceof Atom) {
            const result = (observable as any).get()
            if (result === undefined) {
                // This fixes #1796, because deleting a prop that has an
                // undefined value won't retrigger a observer (no visible effect),
                // the autorun wouldn't subscribe to future key changes (see also next comment)
                adm.has(name as any)
            }
            return result
        }
        // make sure we start listening to future keys
        // note that we only do this here for optimization
        if (isPropertyKey(name)) adm.has(name)
        return target[name]
    }
Copy the code

Step 5: Process the properties and wrap them with their respective decorators

extendObservableObjectWithProperties(proxy, props, decorators, defaultDecorator)
Copy the code

The purpose of this code is to wrap the properties of the object with the corresponding decorator and then assign the properties to the object’s proxy.

function extendObservableObjectWithProperties(target, properties, decorators, defaultDecorator) {
    startBatch()
    try {
        const keys = getPlainObjectKeys(properties)
        for (const key of keys) {
            const descriptor = Object.getOwnPropertyDescriptor(properties, key)!
            
            const decorator =
                decorators && key in decorators
                    ? decorators[key]
                    : descriptor.get
                    ? computedDecorator
                    : defaultDecorator

            constresultDescriptor = decorator! (target, key, descriptor,true)
            if (
                resultDescriptor // otherwise, assume already applied, due to `applyToInstance`
            )
                Object.defineProperty(target, key, resultDescriptor)
        }
    } finally {
        endBatch()
    }
}

Copy the code

When the decorator is user defined decorator category, there are calculated value decorator computedDecorator, the type of action a decorator and default defaultDecoratorobservable decorator.

Only the Action decorator is covered here, and the rest is easier to understand.

In the above use example setAge: action, the decorator is the action.

The code:

const action: IActionFactory = function action(arg1, arg2? , arg3? , arg4?) :any {
    // action(fn() {})
    if (arguments.length === 1 && typeof arg1 === "function")
        return createAction(arg1.name || "<unnamed action>", arg1)
    // action("name", fn() {})
    if (arguments.length === 2 && typeof arg2 === "function") return createAction(arg1, arg2)

    // @action("name") fn() {}
    if (arguments.length === 1 && typeof arg1 === "string") return namedActionDecorator(arg1)

    // @action fn() {}
    if (arg4 === true) {
        // apply to instance immediately
        addHiddenProp(arg1, arg2, createAction(arg1.name || arg2, arg3.value, this))}else {
        return namedActionDecorator(arg2).apply(null.arguments as any)
    }
} as any
Copy the code

Here we pass in four arguments, arg4 === true, to addHiddenProp, which increases the object’s non-traversal property

function addHiddenProp(object: any, propName: PropertyKey, value: any) {
    Object.defineProperty(object, propName, {
        enumerable: false.writable: true.configurable: true,
        value
    })
}
Copy the code

Now let’s see what createAction does:

CreateAction returns a function whose return value is the result of the descriptor, which is the result of setAge() in the example above.

function createAction(actionName: string, fn: Function, ref? : Object) :Function & IAction {
    const res = function() {
        // First save the runInfo information, then execute fn, finally restore the saved information and call endBatch().
        return executeAction(actionName, fn, ref || this.arguments)}; (resas any).isMobxAction = true
   
    return res as any
}

function executeAction(actionName: string, fn: Function, scope? : any, args? : IArguments) {
    // Save information such as derivation and restore it in _endAction()
    const runInfo = _startAction(actionName, scope, args)
    try {
        return fn.apply(scope, args)
    } catch (err) {
        runInfo.error = err
        throw err
    } finally {
        _endAction(runInfo)
    }
}
Copy the code

Ok, now go back to the action defined in the MOBx documentation:

It takes a function and returns a function with the same signature, but wrapped in Transaction, Untracked, and allowStateChanges. In particular, automatic application of Transaction yields huge performance gains, Actions batch changes and notify calculated values and reactions only after the (outermost) action is complete. This ensures that intermediate or unfinished values generated during the action are not visible to the rest of the application until the action completes.

The transaction here refers to the transaction _startAction and _endAction open startBatch () and endBatch (), during the transaction processing globalState. TrackingDerivation = null, That means no dependency collection during action processing (untracked as described) because executing an action might access an Observable property and trigger a GET proxy. The GET proxy does dependency collection, but the action doesn’t need dependency collection. It simply performs an action); RunReactions () is followed in endBatch() (that is, notifying calculated values and reactions when the action described in the description is done); AllowStateChanges is easier to understand. It controls whether an Observable can only change values in actions. Details can be found in config.ts.

Then addHiddenProp (arg1, arg2, createAction (arg1) name | |, arg2, arg3) value, this)) is the action category of action with a decorator action wrapped up and throw it to the proxy object again.

So, observable. Object (object, decorator, configuration item) ends up producing a new object, which is a proxy object.

An array of hijacked

After looking at the proxy for a complex quota object, many of the concepts are already understood, and other types of objects are much easier.

The createObservableArray function is called by the agent Array.

function createObservableArray<T> (
    initialValues: any[] | undefined,
    enhancer: IEnhancer<T>,
    name = "ObservableArray@" + getNextId(),
    owned = false) :IObservableArray<T> {
    const adm = new ObservableArrayAdministration(name, enhancer, owned)
    addHiddenFinalProp(adm.values, $mobx, adm)
    const proxy = new Proxy(adm.values, arrayTraps) as any
    adm.proxy = proxy
    if (initialValues && initialValues.length) {
        const prev = allowStateChangesStart(true)
        / / initialization
        adm.spliceWithArray(0.0, initialValues)
        allowStateChangesEnd(prev)
    }
    return proxy
}
Copy the code

The first step is to create an ADM object. The rest is to call the methods in adm.

The most important method is spliceWithArray, which intercepts the splice operation of the array, enhancer the newly added element, dehancer the deleted element, and reportChanged().

class ObservableArrayAdministration
    implements IInterceptable<IArrayWillChange<any> | IArrayWillSplice<any> >,IListenable {
    atom: IAtom
    values: any[] = []
    interceptors
    changeListeners
    enhancer: (newV: any, oldV: any | undefined) = > any
    dehancer: any
    proxy: any[] = undefined as any
    lastKnownLength = 0

    constructor(name, enhancer: IEnhancer<any>, public owned: boolean) {
        this.atom = new Atom(name || "ObservableArray@" + getNextId())
        this.enhancer = (newV, oldV) = > enhancer(newV, oldV, name + "[...] ")
    }

    getArrayLength(): number {
        this.atom.reportObserved()
        return this.values.length
    }

    setArrayLength(newLength: number) {
        if (typeofnewLength ! = ="number" || newLength < 0)
            throw new Error("[mobx.array] Out of range: " + newLength)
        let currentLength = this.values.length
        if (newLength === currentLength) return
        else if (newLength > currentLength) {
            const newItems = new Array(newLength - currentLength)
            for (let i = 0; i < newLength - currentLength; i++) newItems[i] = undefined // No Array.fill everywhere...
            this.spliceWithArray(currentLength, 0, newItems)
        } else this.spliceWithArray(newLength, currentLength - newLength)
    }

    updateArrayLength(oldLength: number, delta: number) {
        if(oldLength ! = =this.lastKnownLength)
            throw new Error(
                "[mobx] Modification exception: the internal structure of an observable array was changed."
            )
        this.lastKnownLength += delta } spliceWithArray(index: number, deleteCount? : number, newItems? : any[]): any[] { checkIfStateModificationsAreAllowed(this.atom)
        const length = this.values.length

        if (index === undefined) index = 0
        else if (index > length) index = length
        If inedx is less than 0, the value is taken from the end of the list
        else if (index < 0) index = Math.max(0, length + index)

        if (arguments.length === 1) deleteCount = length - index
        else if (deleteCount === undefined || deleteCount === null) deleteCount = 0
        Math.min(deleteCount, length-index) prevents the number of deletions from exceeding the array length
        else deleteCount = Math.max(0.Math.min(deleteCount, length - index))

        if (newItems === undefined) newItems = EMPTY_ARRAY

        if (hasInterceptors(this)) {
            const change = interceptChange<IArrayWillSplice<any>>(this as any, {
                object: this.proxy as any,
                type: "splice",
                index,
                removedCount: deleteCount,
                added: newItems
            })
            if(! change)return EMPTY_ARRAY
            deleteCount = change.removedCount
            newItems = change.added
        }

        // Hijack the new value with enhancer
        newItems = newItems.length === 0 ? newItems : newItems.map(v= > this.enhancer(v, undefined))
        
        // Update the hijacked array to this.values
        // res is the array of deleted elements
        const res = this.spliceItemsIntoValues(index, deleteCount, newItems)

        if(deleteCount ! = =0|| newItems.length ! = =0) this.notifyArraySplice(index, newItems, res)
        // Dehancer for the deleted element
        return this.dehanceValues(res)
    }

    spliceItemsIntoValues(index, deleteCount, newItems: any[]): any[] {
        if (newItems.length < MAX_SPLICE_SIZE) {
            // splice returns the deleted element
            return this.values.splice(index, deleteCount, ... newItems) }else {
            // res is the deleted element
            const res = this.values.slice(index, index + deleteCount)
            this.values = this.values
                .slice(0, index)
                .concat(newItems, this.values.slice(index + deleteCount))
            return res
        }
    }

    notifyArrayChildUpdate(index: number, newValue: any, oldValue: any) {
        / / to omit...
        this.atom.reportChanged()
    }

    notifyArraySplice(index: number, added: any[], removed: any[]) {
        / / to omit...
        this.atom.reportChanged()
    }
}
Copy the code

Step 2: Throw adm to adm.values property $mobx;

addHiddenFinalProp(adm.values, $mobx, adm)
Copy the code

Step 3: Proxy objects

const proxy = new Proxy(adm.values, arrayTraps)
Copy the code

The get and set methods of agents are in arrayTraps.

const arrayTraps = {
    get(target, name) {
        if (name === $mobx) return target[$mobx]
        if (name === "length") return target[$mobx].getArrayLength()
        if (typeof name === "number") {
            return arrayExtensions.get.call(target, name)
        }
        if (typeof name === "string"&&!isNaN(name as any)) {
            return arrayExtensions.get.call(target, parseInt(name))
        }
        if (arrayExtensions.hasOwnProperty(name)) {
            return arrayExtensions[name]
        }
        return target[name]
    },
    set(target, name, value): boolean {
        if (name === "length") {
            target[$mobx].setArrayLength(value)
        }
        if (typeof name === "number") {
            arrayExtensions.set.call(target, name, value)
        }
        if (typeof name === "symbol" || isNaN(name)) {
            target[name] = value
        } else {
            // numeric string
            arrayExtensions.set.call(target, parseInt(name), value)
        }
        return true
    },
    preventExtensions(target) {
        fail(`Observable arrays cannot be frozen`)
        return false}}Copy the code

You can see that in addition to calling adm’s methods as usual, you also use the arrayExtensions, which encapsulate the basic operations on the array, nature, and which or which adm’s methods are called.

// Call the property $mobx on the whole object --adm to operate on values
const arrayExtensions = {
        intercept(handler: IInterceptor<IArrayWillChange<any> | IArrayWillSplice<any>>): Lambda {
            return this[$mobx].intercept(handler)
        },
        observe(
            listener: (changeData: IArrayChange<any> | IArraySplice<any>) = > void,
            fireImmediately = false
        ): Lambda {
            const adm: ObservableArrayAdministration = this[$mobx]
            return adm.observe(listener, fireImmediately)
        },
        clear(): any[] {
            return this.splice(0)
        },
        replace(newItems: any[]) {
            const adm: ObservableArrayAdministration = this[$mobx]
            return adm.spliceWithArray(0, adm.values.length, newItems)
        },
        toJS(): any[] {
            return (this as any).slice()
        },
        toJSON(): any[] {
            // Used by JSON.stringify
            return this.toJS() }, splice(index: number, deleteCount? : number, ... newItems: any[]): any[] {const adm: ObservableArrayAdministration = this[$mobx]
            switch (arguments.length) {
                case 0:
                    return []
                case 1:
                    return adm.spliceWithArray(index)
                case 2:
                    return adm.spliceWithArray(index, deleteCount)
            }
            returnadm.spliceWithArray(index, deleteCount, newItems) }, spliceWithArray(index: number, deleteCount? : number, newItems? : any[]): any[] {const adm: ObservableArrayAdministration = this[$mobx]
            returnadm.spliceWithArray(index, deleteCount, newItems) }, push(... items: any[]): number {const adm: ObservableArrayAdministration = this[$mobx]
            adm.spliceWithArray(adm.values.length, 0, items)
            return adm.values.length
        },

        pop() {
            return this.splice(Math.max(this[$mobx].values.length - 1.0), 1) [0]
        },

        shift() {
            return this.splice(0.1) [0] }, unshift(... items: any[]): number {const adm = this[$mobx]
            adm.spliceWithArray(0.0, items)
            return adm.values.length
        },

        reverse(): any[] {
            constclone = (<any>this).slice() return clone.reverse.apply(clone, arguments) }, sort(compareFn? : (a: any, b: any) => number): any[] { const clone = (<any>this).slice() return clone.sort.apply(clone, arguments) }, remove(value: any): boolean { const adm: ObservableArrayAdministration = this[$mobx] const idx = adm.dehanceValues(adm.values).indexOf(value) if (idx > -1) { this.splice(idx, 1) return true } return false }, get(index: number): any | undefined { const adm: ObservableArrayAdministration = this[$mobx] if (adm) { if (index < adm.values.length) { adm.atom.reportObserved() return  adm.dehanceValue(adm.values[index]) } } return undefined }, set(index: number, newValue: any) { const adm: ObservableArrayAdministration = this[$mobx] const values = adm.values if (index < values.length) { // update at index in  range checkIfStateModificationsAreAllowed(adm.atom) const oldValue = values[index] if (hasInterceptors(adm)) { const change = interceptChange<IArrayWillChange<any>>(adm as any, { type: "update", object: adm.proxy as any, // since "this" is the real array we need to pass its proxy index, newValue }) if (! change) return newValue = change.newValue } newValue = adm.enhancer(newValue, oldValue) const changed = newValue ! == oldValue if (changed) { values[index] = newValue adm.notifyArrayChildUpdate(index, newValue, oldValue) } } else if (index === values.length) { // add a new item adm.spliceWithArray(index, 0, [newValue]) } else { // out of bounds throw new Error( `[mobx.array] Index out of bounds, ${index} is larger than ${values.length}` ) } } }Copy the code

Arrays also have some built-in methods, which Mobx takes a step further and places in the arrayExtensions.

; ["concat"."every"."filter"."forEach"."indexOf"."join"."lastIndexOf"."map"."reduce"."reduceRight"."slice"."some"."toString"."toLocaleString"
].forEach(funcName= > {
    arrayExtensions[funcName] = function() {
        const adm: ObservableArrayAdministration = this[$mobx]
        // There are reportObserved and reportChanged functions in Atom
        adm.atom.reportObserved()
        const res = adm.dehanceValues(adm.values)
        return res[funcName].apply(res, arguments)}})Copy the code

With dependency collection, change notification capabilities, array objects can work with Derivation once they are propped in.

The map of hijacked

The code for ObservableMap is verbose, step-by-step disassembly analysis.

Let’s look at its constructor first:

This._data is a proxy object for initialData.

This._hasmap is a change of keys in the cache map — new or removed state.

 constructor( initialData? : IObservableMapInitialValues<K, V>, public enhancer: IEnhancer<V> = deepEnhancer, public name = "ObservableMap@" + getNextId() ) {this._data = new Map(a)// this.get() calls this.has()
        // this.has() calls this._hasmap.set () to set the value
        // So only when observerablemap.get (' XXX ') does _hasMap have 'XXX' attributes and values
        // _updateHasMapEntry() is called when an attribute is added or deleted in the map,
        // _updateHasMapEntry() sets the value of the new property 'XXX' to 'true' and the value of the new property 'XXX' to 'false'
        // For example, autorun(() => console.log(counterstore.testmap.get (' XXX ')));
        // in this case, _hasMap has a value;

        @action func() {this.testmap.get (' XXX ')}}
        // this will not put 'XXX' in _hasMap

        // Summary: _hasMap is used to store keys added or deleted (this only works with autorun reaction)
        // Thus _hasMap is the change of keys in the cache map -- new or removed state
        this._hasMap = new Map(a)// Assign the attributes and values of the initial data to this._data
        // Merge calls this.set()-->this._addValue(), making the _data property ObservableValue
        this.merge(initialData)
    }
Copy the code

Merge (initialData) is the iterative assignment of initialData to this._data. If it is a new attribute, first change the value into Observable, and then inform reportChanged() to trigger derivation. If the value is update, notify this.reportchanged () after processing the new value with enhancer.

// Assign the attributes of other to this, and return this
merge(other: ObservableMap<K, V> | IKeyValueMap<V> | any): ObservableMap<K, V> {
    if (isObservableMap(other)) {
    	other = other.toJS()
	}
    // Transaction starts a transaction. The view is not updated during the transaction
    transaction((a)= > {
        if (isPlainObject(other))
            getPlainObjectKeys(other).forEach(key= > this.set((key as any) as K, other[key]))
        else if (Array.isArray(other)) other.forEach(([key, value]) = > this.set(key, value))
        else if (isES6Map(other)) {
            if(other.constructor ! = =Map)
                fail("Cannot initialize from classes that inherit from Map: " + other.constructor.name)
            other.forEach((value, key) = > this.set(key, value))
        } else if(other ! = =null&& other ! = =undefined)
            fail("Cannot initialize map from " + other)
    })
    return this
}

set(key: K, value: V) {
    const hasKey = this._has(key)

    if (hasKey) {
        this._updateValue(key, value)
    } else {
        this._addValue(key, value)
    }
    return this
}
Copy the code
private _updateValue(key: K, newValue: V | undefined) {
    const observable = this._data.get(key)!
          newValue = (observable as any).prepareNewValue(newValue) as V
          if(newValue ! == globalState.UNCHANGED) { observable.setNewValue(newValueas V)
          }
}

private _addValue(key: K, newValue: V) {
    checkIfStateModificationsAreAllowed(this._keysAtom)
    transaction((a)= > {
        const observable = new ObservableValue(
            newValue,
            this.enhancer,
            `The ${this.name}.${stringifyKey(key)}`.false
        )
        this._data.set(key, observable)
        newValue = (observable as any).value // value might have been changed
        this._updateHasMapEntry(key, true)
        this._keysAtom.reportChanged()
    })
}
Copy the code

ObservableMap also defines map methods like Replace, Clear, and entries, which hijack the native Map methods.

The set of hijacked

As usual, first look at what its constructor does:

constructor( initialData? : IObservableSetInitialValues<T>, enhancer: IEnhancer<T> = deepEnhancer, public name = "ObservableSet@" + getNextId() ) {if (typeof Set! = ="function") {
            throw new Error(
                "mobx.set requires Set polyfill for the current browser. Check babel-polyfill or core-js/es6/set.js")}this.enhancer = (newV, oldV) = > enhancer(newV, oldV, name)

        if (initialData) {
            this.replace(initialData)
        }
    }
Copy the code

Redefining the enhancer function and handling initialData.

 replace(other: ObservableSet<T> | IObservableSetInitialValues<T>): ObservableSet<T> {
        if (isObservableSet(other)) {
            other = other.toJS()
        }

        transaction((a)= > {
            if (Array.isArray(other)) {
                this.clear()
                other.forEach(value= > this.add(value))
            } else if (isES6Set(other)) {
                this.clear()
                other.forEach(value= > this.add(value))
            } else if(other ! = =null&& other ! = =undefined) {
                fail("Cannot initialize set from " + other)
            }
        })

        return this
    }
Copy the code

Replace is simple, makes some judgments, and finally calls add:

Again, we see the familiar this._data, as in ObservableMap.

The add function is also simple; enhancer processes it, puts it into this._data, and reports the change.

add(value: T) {
    checkIfStateModificationsAreAllowed(this._atom)
    if (!this.has(value)) {
        transaction((a)= > {
            this._data.add(this.enhancer(value, undefined))
            this._atom.reportChanged()
        })
    }

    return this
}
Copy the code
Hijacking of basic data types

String, Boolean, number hijacked with box, and finally called ObservableValue().

Overwrite the original value after enhancer processing.

Of course, the ObservableValue class also proxies for get and set operations.

constructor(
        value: T,
        public enhancer: IEnhancer<T>,
        public name = "ObservableValue@" + getNextId(),
        notifySpy = true,
        private equals: IEqualsComparer<any> = comparer.default
    ) {
        super(name)
        this.value = enhancer(value, undefined, name)
    }
Copy the code
computedValue

Let’s take a look at an example:

class OrderLine {
    @observable price = 0;
    @observable amount = 1;

    constructor(price) {
        this.price = price;
    }

    @computed get total() {
        return this.price * this.amount; }}Copy the code

In addition, both Observable. object and extendObservable automatically derive getter properties into computed properties, so the following is sufficient:

const orderLine = observable.object({
    price: 0.amount: 1,
    get total() {
        return this.price * this.amount
    }
})
Copy the code

Take a look at computed methods first:

const computed: IComputed = function computed(arg1, arg2, arg3) {
    if (typeof arg2 === "string") {
        // @computed
        return computedDecorator.apply(null.arguments)}if(arg1 ! = =null && typeof arg1 === "object" && arguments.length === 1) {
        // @computed({ options })
        return computedDecorator.apply(null.arguments)}// computed(expr, options?)
    const opts: IComputedValueOptions<any> = typeof arg2 === "object" ? arg2 : {}
    opts.get = arg1
    opts.set = typeof arg2 === "function" ? arg2 : opts.set
    opts.name = opts.name || arg1.name || "" /* for generated name */

    return new ComputedValue(opts)
}
Copy the code

There are three branches, executing a computedDecorator as a decorator with no parameters and returning it, executing a computedDecorator with parameters and returning it. When computed as computed(expr, options?) , returns a new ComputedValue(OPTS).

Let’s take a look at what the computedDecorator does.

The createPropDecorator function is called, and the second argument passes in a callback function. The callback function has the familiar asObservableObject that returns the ADM object and hangs the ADM on the $mobx property of the instance. We then call addComputedProp to add the calculated properties of the instance.

const computedDecorator = createPropDecorator(
    false,
    (
        instance: any,
        propertyName: PropertyKey,
        descriptor: any,
        decoratorTarget: any,
        decoratorArgs: any[]
    ) => {
        const { get, set } = descriptor 
        const options = decoratorArgs[0] || {}
        asObservableObject(instance).addComputedProp(instance, propertyName, {
            get,
            set,
            context: instance, ... options }) } )Copy the code

AddComputedProp puts computed properties of computed decoration into this.values with a value of type ComputedValue. The get property in options is assigned to this.derivation in ComputedValue. The value of the calculated property is recalculated when calculating observable changes that the property depends on.

Finally, defineProperty is used to redefine the calculation attribute of target class and do some get and set hijacking proxy.

As mentioned earlier, this.value is used in adm.

GenerateComputedPropConfig redefined attribute descriptor set and get functions, or call the read, write, adm function actually.

addComputedProp(
        propertyOwner: any, 
        propName: PropertyKey,
        options: IComputedValueOptions<any>
    ) {
        const { target } = this
        options.name = options.name || `The ${this.name}.${stringifyKey(propName)}`
        this.values.set(propName, new ComputedValue(options))
        if (propertyOwner === target || isPropertyConfigurable(propertyOwner, propName))
            Object.defineProperty(propertyOwner, propName, generateComputedPropConfig(propName))
    }
Copy the code

The createPropDecorator returns a decorator factory function that, when executed, returns either a property descriptor (@computed with no parameters) ora decorator function (@computed({options}) with parameters).

function createPropDecorator(propertyInitiallyEnumerable: boolean, propertyCreator: PropertyCreator) {
    return function decoratorFactory() {
        let decoratorArguments: any[]

        const decorator = function decorate(target: DecoratorTarget, prop: string, descriptor: BabelDescriptor | undefined, applyImmediately? : any) {
            if (applyImmediately === true) {
                propertyCreator(target, prop, descriptor, target, decoratorArguments)
                return null
            }
            
            if (!Object.prototype.hasOwnProperty.call(target, mobxPendingDecorators)) {
                constinheritedDecorators = target[mobxPendingDecorators] addHiddenProp(target, mobxPendingDecorators, { ... inheritedDecorators }) }/** * createPropDecorator passed in the second argument, * then put target[mobxPendingDecorators]! In the [prop] property, * is used by initializeInstance with */target[mobxPendingDecorators]! [prop] = { prop, propertyCreator, descriptor,decoratorTarget: target,
                decoratorArguments
            }
            return createPropertyInitializerDescriptor(prop, propertyInitiallyEnumerable)
        }

        if (quacksLikeADecorator(arguments)) {
            // @decorator has no arguments
            decoratorArguments = EMPTY_ARRAY
            // Return descriptor descriptor (decorator.apply(null, arguments))
            return decorator.apply(null.arguments as any)
        } else {
            // @decorator(args) has arguments
            // decoratorArguments assigned here, used in decorator functions (using closures)
            decoratorArguments = Array.prototype.slice.call(arguments)
            return decorator
        }
    }
}
Copy the code

CreatePropertyInitializerDescriptor caches mobx store attributes or methods defined in the descriptor, if no cache is to generate a new descriptor cache, and new descriptors of the get and set do intercept processing, InitializeInstance is called. This function is executed only once and is used to bring all the properties on target

function createPropertyInitializerDescriptor(prop: string, enumerable: boolean) :PropertyDescriptor {
    const cache = enumerable ? enumerableDescriptorCache : nonEnumerableDescriptorCache
    return (
        cache[prop] ||
        (cache[prop] = {
            configurable: true.enumerable: enumerable,
            get() {
                initializeInstance(this)
                return this[prop]
            },
            set(value) {
                initializeInstance(this)
                this[prop] = value
            }
        })
    )
}
Copy the code

Target [mobxPendingDecorators] is all properties in the Mobx Store decorated with @Observable and @computed to observe objects and calculate properties. I’m just going to calculate the properties, so I’m going to do propertyCreator for those properties once in initializeInstance, AddComputedProp (Instance, propertyName, {get,set, Context: instance,… Options}) is intended to convert an attribute to a calculated value and hijack its set and GET operations.

function initializeInstance(target: DecoratorTarget) {
    if (target[mobxDidRunLazyInitializersSymbol] === true) return
    
    const decorators = target[mobxPendingDecorators]
    if (decorators) {
        addHiddenProp(target, mobxDidRunLazyInitializersSymbol, true)
        // Build property key array from both strings and symbols
        const keys = [...Object.getOwnPropertySymbols(decorators), ...Object.keys(decorators)]
        for (const key of keys) {
            const d = decorators[key as any]
            d.propertyCreator(target, d.prop, d.descriptor, d.decoratorTarget, d.decoratorArguments)
        }
    }
}
Copy the code

Next, ComputedValue implements both IDerivation and IObservable interfaces. It is both observer and observed, so its member variables are collections of observable variables and derivatives.

Call this.variable.call (this.scope) so we call this.variable.call (this.scope). The derivation is the @computed get attribute function that returns the executed result assigned to this.value;

The second branch: First, reportObserved(this) reports being observed and places itself in the derivation newObserving queue. Then call this.trackandcompute (), as the name suggests: Collect dependencies and calculate values. Call this.computeValue(true) to collect dependencies and end up with trackDerivedFunction(this, this. this.scope, this.scope). If the calculated value changes, call propagateChangeConfirmed(Observable: IObservable), sets the observer’s dependency state to stale (D. dendenciesState = iderivationstate.stale).

public get(): T {
        if (this.isComputing) fail(`Cycle detected in computation The ${this.name}: The ${this.derivation}`)
        // Initialize the dependency that gets the bound calculated property, or get the calculated property directly in the action
        if (globalState.inBatch === 0 && this.observers.size === 0&&!this.keepAlive) {
            if (shouldCompute(this)) {
                this.warnAboutUntrackedRead()
                startBatch() 
                this.value = this.computeValue(false)
                endBatch()
            }
        } else {
            In the reaction. RunReaction processing logic, the second conditional branch is entered
            // ComputedValue not only acts as a reportObserved to reaction
            reportObserved(this)
            // It is also a derived class of IDerivation, which is evaluated by trackAndCompute (which calls the trackDerivedFunction)
            // If trackAndCompute returns true, that is, the value has changed, report change to observers who are monitoring themselves
            if (shouldCompute(this)) if (this.trackAndCompute()) propagateChangeConfirmed(this)}const result = this.value!

        if (isCaughtException(result)) throw result.cause
        return result
    }

Copy the code
computeValue(track: boolean) {
        this.isComputing = true
        globalState.computationDepth++
        let res: T | CaughtException
        if (track) {
            res = trackDerivedFunction(this.this.derivation, this.scope)
        } else {
            if (globalState.disableErrorBoundaries === true) {
                res = this.derivation.call(this.scope)
            } else {
                try {
                    res = this.derivation.call(this.scope)
                } catch (e) {
                    res = new CaughtException(e)
                }
            }
        }
        globalState.computationDepth--
        this.isComputing = false
        return res
    }
Copy the code

There is one detail about the calculated values: When computedValue is derivation, it calls propagateMaybeChanged when the Observable it relies on changes. This method is used at computedValue onBecomeStale() {propagateMaybeChanged(this)}.

Call link: Value changed –> D.becomestale () –> propagateMaybeChanged

First change the state of lowestObserverState to POSSIBLY_STALE, which means that the calculated value may change. Only the calculated value has this state, because the particularity of the calculated value is both the observer and the observed, and it needs to be aware of the change of the dependent object. Meanwhile, once its own value changes, Astute notifying observers that they are astute, too, so an intermediate state is likely to be unstable, and will allow them to perform actions only if the calculated values are truly changed, which is a major performance optimization. DependenciesState changed to POSSIBLY_STALE, indicating that observers may need to re-execute their logic. 5, propagateChangeConfirmed When POSSIBLY_STALE becomes STALE, propagateChangeConfirmed

function propagateMaybeChanged(observable: IObservable) {
    if(observable.lowestObserverState ! == IDerivationState.UP_TO_DATE)return
    observable.lowestObserverState = IDerivationState.POSSIBLY_STALE

    observable.observers.forEach(d= > {
        if (d.dependenciesState === IDerivationState.UP_TO_DATE) {
            d.dependenciesState = IDerivationState.POSSIBLY_STALE
            if(d.isTracing ! == TraceMode.NONE) { logTraceInfo(d, observable) } d.onBecomeStale() } }) }Copy the code

When derivation relies on this computedValue and runReaction triggers get, it goes to propagateChangeConfirmed (the calculated value does change), changing the state POSSIBLY_STALE to STALE.

// ComputedValue Called when the value changes to recalculate
function propagateChangeConfirmed(observable: IObservable) {
    if (observable.lowestObserverState === IDerivationState.STALE) return
    observable.lowestObserverState = IDerivationState.STALE

    observable.observers.forEach(d= > {
        if (d.dependenciesState === IDerivationState.POSSIBLY_STALE)
            d.dependenciesState = IDerivationState.STALE
        else if (
            // In the dependency collection phase
            d.dependenciesState === IDerivationState.UP_TO_DATE
        )
            observable.lowestObserverState = IDerivationState.UP_TO_DATE
    })
}
Copy the code

Note: A comment on the computedValue implementation is quoted in the source code:

Implementation description:

  1. First time it’s being accessed it will compute and remember result

​ give back remembered result until 2. happens

  1. First time any deep dependency change, propagate POSSIBLY_STALE to all observers, wait for 3.

  2. When it’s being accessed, recompute if any shallow dependency changed.

​ if result changed: propagate STALE to all observers, that were POSSIBLY_STALE from the last step.

​ go to step 2. either way

With the above foundation, you can go to see the source code, in the more difficult or details of the place, you can write a demo open browser debugging source code, there will always be a suddenly enlightened feeling.

Welcome to star my source code interpretation series mobx, thanks again!

Refer to the article

lawler61

  • How about going from zero to Observable an object
  • Mobx source code interpretation (b) : Have observed object, other types will be far behind
  • Mobx dependency collection: subscription-publish model
  • Autorun and Reaction
  • Mobx-react mobx react mobx React

Repair the van

  • Mobx source code analysis (a) to construct responsive data
  • Mobx source code analysis (2) subscription responsive data