Interpretation of mobx source code -- autorun and observable

The first time I read the source code, I may have misunderstood something. I hope the bosses can help me correct it. At first, I looked at 6. Later, I found that there is still a little gap between observable and 5. Therefore, there may be 6 source code in the "autorun" part, but the gap is not large.

1. Basic concepts of mobx

Observable observable

Observer observation

Reaction response

var student = mobx.observable({
    name: 'Zhang San',
});

mobx.autorun(() => {
    console.log('Zhang San's name:', student.name);
});

2. Principle of mobx

1. In a responsive function (for example, one or more observable objects are usually accessed in autorun above),

  • 1) autorun first creates an instance object of reaction type, and a callback function of a responsive function through the parameter track.
  • 2) Then execute reaction.schedule_ Method implements the callback function, calling the observer observable.get method in the callback function, triggering the reportObserved method.
  • 3) The reportObserved method collects the observavle object to globalState.trackingDerivation.newObserving_ In the queue (globalState.trackingDerivation is now equivalent to the reaction object)
  • 4) Handle the dependency between reaction and observable, and traverse reaction.newObserving_ Property in newObserving_ Every observable.observers in the queue_ Property to add the current reaction object.

2. If the observable value changes, call the observable object set method to trigger the propagateChange method. In the propagateChange method, traverse observable.observers_ Property, execute the reaction.onBecomeStale method in turn, and execute the above 2) 3) 4) again.

3. Source code interpretation - autorun

The following is the deleted code

3.1 autorun

export function autorun(
    view: (r: IReactionPublic) => any,// Callback function of autoruan function
    opts: IAutorunOptions = EMPTY_OBJECT
): IReactionDisposer {
    
    const name: string = "Autorun"
    const runSync = !opts.scheduler && !opts.delay
    
    // First, create an object of Reaction type. Its main function is to control the execution of tasks
    let reaction = new Reaction(
        name,
        function (this: Reaction) {
            this.track(reactionRunner)
        },
        opts.onError,
        opts.requiresObservable
    )

    function reactionRunner() { view(reaction) } // view is the callback of the autorun function

    reaction.schedule_() // Perform a deployment now    
    
    return reaction.getDisposer_() // Used to clean up autorun during execution
}

From the above source code, we can see that autorun mainly does the following three actions

  • 1) The main function of creating a Reaction type object is to control the execution of tasks
  • 2) Assign view, the callback function of auto, to reaction.track
  • 3) Execute a deployment immediately. At this time, you should understand the document that "when autorun is used, the provided functions are always triggered immediately"

3.2 reaction.schedule_ Source code

schedule_() {
    if (!this.isScheduled_) {
        this.isScheduled_ = true
        globalState.pendingReactions.push(this) // The current reaction object is listed
        runReactions() // All reaction objects in the queue execute runReaction_ method
    }
}

function runReactionsHelper() {
   let remainingReactions = allReactions.splice(0)
   for (let i = 0, l = remainingReactions.length; i < l; i++)
        remainingReactions[i].runReaction_()
}

schedule_ The method does two things

  • 1) The current reaction object is listed
  • 2) All reaction objects in the queue execute runReaction_ method

3.3 runReaction_ Source code

runReaction_() {
    startBatch() // Start a transaction
    this.isScheduled_ = false
   
    if (shouldCompute(this)) {// derivation.dependenciesState_ The default is - 1 (not tracked) 
       this.onInvalidate_()
    }
    endBatch() // Close the next level transaction. startBatch and endBatch always appear in pairs
}

If you look at the code above, you can find onInvalidate_ The constructor is passed in when initializing Reaction. In fact, the reaction.track method is called when initializing Reaction

track(fn: () => void) {
    startBatch()
    ...
    const result = trackDerivedFunction(this, fn, undefined) // Execute task update dependency
    ...
    endBatch()
}

3.4 the track method mainly calls trackDerivedFunction

export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
    ...
    globalState.trackingDerivation = derivation  // Mount the derivation (equivalent to the reaction object here) to the global variable so that other members can access the derivation
    ...
    // Execute the callback method passed by reaction. You can see from the code that the callback method of autoran function is executed
    // Generally, one or more observable objects will be called in the callback to trigger the observable.get method and then the reportObserved method
    let result = f.call(context)
    ...
    globalState.trackingDerivation = prevTracking
    bindDependencies(derivation) // Update the dependency between observable and race
    return result
}

Because autorun calls the student.name variable back, the "." here is actually a get operation; Once the get operation is designed, the observer supervising the name attribute will execute the reportObserved method (this will be highlighted later when Oobservable is introduced).

3.5 reportObserved source code

export function reportObserved(observable: IObservable): boolean {
    ...
    const derivation = globalState.trackingDerivation
    if (derivation !== null) {

        if (derivation.runId_ !== observable.lastAccessedBy_) {
            
             // Last accessed by the more observed_ Attribute (transaction id), which is used to avoid repeated operations
            observable.lastAccessedBy_ = derivation.runId_ 
            
            // Update the newObserving property of derivation (here reaction) to add the observed to the queue
            // Subsequent derivation and observable update dependencies depend on this attribute
            derivation.newObserving_![derivation.unboundDepsCount_++] = observable 

            ...
        }
        return true
    } else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
        queueForUnobservation(observable)
    }

    return false
}

In the above code, we mainly focus on the operations that affect derivation

  • 1) Update lastAccessedBy of observable_ Attribute (transaction id), which is used to avoid repeated operations.
  • 2) Update the newObserving attribute of derivation (here, reaction) and add observable to the queue. The dependency of subsequent derivation and observable updates depends on this attribute

Then, after the autorun task is completed, derivation starts to update the dependency with the observable observer

3.6 bindDependencies source code

function bindDependencies(derivation: IDerivation) {
    const prevObserving = derivation.observing_
     
    // derivation.newObserving_ Queue of observable objects dependent on derivation
    const observing = (derivation.observing_ = derivation.newObserving_!)
    let lowestNewObservingDerivationState = IDerivationState_.UP_TO_DATE_ // The default is 0

    let i0 = 0,
        l = derivation.unboundDepsCount_
    for (let i = 0; i < l; i++) {

        /**
         * The following is a process of weight removal 
         * observable.diffValue_The default is 0
         * Set to 1 when looping. Because observing is an array of objects of type Observable, diffvalues of the same value in different positions_ Will become 1
         * After traversing the duplicate items, it will not enter the judgment below, i0 will not++
         * When traversing to non duplicate items (items with diffValue_; 0), this item is directly filled in the position corresponding to i0
         * In this way, the array cycle is completed, i0 is the number of non duplicate items, and observing.length = i0 deletes redundant items
         */
        
        const dep = observing[i]
        if (dep.diffValue_ === 0) {
            dep.diffValue_ = 1
            if (i0 !== i) observing[i0] = dep
            i0++
        }
    }
    observing.length = i0

    derivation.newObserving_ = null // newObserving is null

    /**
     * prevObserving And observing are observable objects 
     * At this time, if you observe the diffValue of the existing observable object after the loop above is completed_ All 1
     * In the prevObserving queue, if it is diffValue_ Still 0 indicates that the current derivation does not depend on this observable object
     */
    l = prevObserving.length
    while (l--) {
        const dep = prevObserving[l];
        if (dep.diffValue_ === 0) {
            // The current derivation no longer depends on this observable object. Remove it from observable.observers_ deleted in
            removeObserver(dep, derivation)  
        }
        dep.diffValue_ = 0 // Set the diffValue of observable in the prevObserving queue_ All set to 0
    }

    
    while (i0--) {
        const dep = observing[i0]
        
        // If observing is still 1, this observable object is not in the prevObserving queue 
        if (dep.diffValue_ === 1) {
            dep.diffValue_ = 0
            // In observable.observers_ Adds the current derivation object to the
            addObserver(dep, derivation) 
        }
    }
    // Update derivation.observing to the latest dependency (and de duplicate) through the above three cycles,
    // And in the observers of the observable object that is no longer dependent_ delete the current derivation object in
    // Create the observers of the dependent observable object_ add current derivation object in
}

The value of the observable object changes in response to the observer
As mentioned above, once the observable value changes, the observable.get method will be triggered, and then the propagateChange method will be triggered. The source code of propagateChange is as follows

export function propagateChanged(observable: IObservable) {
    ...
    observable.observers_.forEach(d => {
        ...
            d.onBecomeStale_()
        ...
    })
    
}

observable.observers_ Stored is the derivation object that has a dependency on the observable object. In the propagateChanged method, traverse the observers_ Execute onBecomeStale of derivation object_ Method, let's take a look at onBecomeStale_ Source code

3.7 onBecomeStale_ Source code

onBecomeStale_() {
    this.schedule_()
}

this.schedule_ Are you familiar with it? Look at the code above and find that reaction.schedule is called when creating a reaction object in the AutoRun function_ (). So now understand that propagateChanged calls onBecomeStale_ It is to let the reaction execute the previous deployment operation again (that is, execute the callback of autorun and deal with dependencies);

4. Next, start looking at the observable section

4.1 alias of observable createObservable

export const observable: IObservableFactory &
    IObservableFactories & {
        enhancer: IEnhancer<any>
    } = createObservable as any // Alias of observable createObservable

// Copy the properties of observableFactories to observable
Object.keys(observableFactories).forEach(name => (observable[name] = observableFactories[name]))
  • 1) First, observable is a function, which is the same as createObservable.
  • 2) observable copies the properties of observableFactories.
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
}

The createObservable method plays the role of forwarding the incoming object to the specific conversion function.
Briefly analyze the specific transformation mode

  • 1) arguments[1] = = = "string" | | typeof arguments[1] = = "symbol" uses the decorator @ observable, and the decorator's parameters (target,prop,descriptor), where arguments[1], that is, prop is the attribute and the name is the string type
  • 2) isObservable(v) has been converted to an observation value. No further conversion is required
  • 3) observable.object, observable.array, observable.map and observable.set respectively call specific conversion methods according to the type of the passed in parameters
  • 4) The user is prompted for the original type. It is recommended to use the observable.box method

4.2 observable.box

observable.box is described in this document.

observable.box converts ordinary values into observable values, as shown in the following example.

const name = observable.box("Zhang San");

console.log(name.get());
// Output 'Zhang San'

name.observe(function(change) {
    console.log(change.oldValue, "->", change.newValue);
});

name.set("Li Si");
// Output 'Zhang San - > Li Si'

observable.box retrun an object of type ObservableValue.

box<T = any>(value?: T, options?: CreateObservableOptions): IObservableValue<T> {
    const o = asCreateObservableOptions(options) // Format input parameters
    // get set observe intercept method of ObservableValue
    return new ObservableValue(value, getEnhancerFromOptions(o), o.name, true, o.equals)
},

The "name.set(" Li Si ") in the case calls the set method of ObservableValue. I'll focus on ObservableValue later.

4.3 core class ObservableValue

ObservableValue inherits Atom atomic class. First, sort out the main capabilities of Atom and ObservableValue.

Atom
 
public reportObserved(): boolean {
    return reportObserved(this)
}

public reportChanged() {
    startBatch()
    propagateChanged(this)
   endBatch()
}
ObservableValue

public set(newValue: T) {
    const oldValue = this.value
    newValue = this.prepareNewValue(newValue) as any
    if (newValue !== globalState.UNCHANGED) {
        const oldValue = this.value
        this.value = newValue
        this.reportChanged()
        ...
    }
}
public get(): T {
    this.reportObserved()
    return this.dehanceValue(this.value)
}

intercept
observe

reportObserved and propagateChanged were introduced when combing autorun.

  • 1) reportObserved: the call observation value is the dependency used to update derivation and observable.
  • 2) propagateChanged: when the observation value changes, execute onBecomeStale method and re execute the deployment operation based on the derivation stored in observers of observable object.
  • 3) The Observablevalue set modifies the value and calls Atom's reportChanged method to trigger propagateChanged.
  • 4) get of Observablevalue calls Atom's reportObserved method to trigger reportObserved while obtaining the value value.

Therefore, in the above case, "name.set(" Li Si ");" will trigger the propagateChanged method and execute the dependent derivation to re deploy

Next, let's take a look at what we did when we created new ObservableValue?

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

ObservableValue's constructor calls enhancer to process value, and enhancer is the parameter getEnhancerFromOptions(o) passed by the parameter to create the ObservableValue type object. Getenhancerfromoptions returns deepEnhancer by default.

function getEnhancerFromOptions(options: CreateObservableOptions): IEnhancer<any> {
    return options.defaultDecorator
        ? options.defaultDecorator.enhancer
        : options.deep === false
        ? referenceEnhancer
        : deepEnhancer
}

The main contents of gdeepnenhancer are as follows.

export function deepEnhancer(v, _, name) {
    if (isObservable(v)) return v
    if (Array.isArray(v)) return observable.array(v, { name })
    if (isPlainObject(v)) return observable.object(v, undefined, { name })
    if (isES6Map(v)) return observable.map(v, { name })
    if (isES6Set(v)) return observable.set(v, { name })
    return v
}

Does the deepEnhancer look familiar? If you look up, you can see that it is very similar to the createObservable function. It plays the role of forwarding the incoming object to the specific conversion function. Therefore, to understand observable, we mainly need to understand these conversion functions. Next, we mainly analyze observable.object.

4.4 observable.object

object<T = any>(
        props: T,
        decorators?: { [K in keyof T]: Function },
        options?: CreateObservableOptions
    ): T & IObservableObject {
  
    const o = asCreateObservableOptions(options)
    if (o.proxy === false) {
        return extendObservable({}, props, decorators, o) as any
    } else {
        const defaultDecorator = getDefaultDecoratorFromObjectOptions(o)
        const base = extendObservable({}, undefined, undefined, o) as any
        const proxy = createDynamicObservableObject(base)
        extendObservableObjectWithProperties(proxy, props, decorators, defaultDecorator)
        return proxy
    }
}

o. When Proxy is true, there is only one more step Proxy, and the rest of the work is basically similar, so we can mainly focus on the extendObservable method.

extendObservable mediation mainly uses getDefaultDecoratorFromObjectOptions, asObservableObject and extendObservableObjectWithProperties methods. Because getdefaultdecoratorfromobjectoptions is associated with extendobservableobjectwithproperties, let's look at asobservableobject first and then the other two methods.

4.5 extendObservable

export 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) // Returns the deepDecorator decorator by default
    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
}

4.6 asObservableObject

asObservableObject method:

  • 1) Create an instance whose object amd is the ObservableObjectAdministration class.
  • 1) amd assigned to target[$mobx]
  • 2) Return amd;
export function asObservableObject(
    target: any,
    name: PropertyKey = "",
    defaultEnhancer: IEnhancer<any> = deepEnhancer
): ObservableObjectAdministration {
    const adm = new ObservableObjectAdministration(
        target,
        new Map(),
        stringifyKey(name),
        defaultEnhancer
    )
    addHiddenProp(target, $mobx, adm)
    return adm
}

4.7 extendObservableObjectWithProperties

extendObservableObjectWithProperties: loop the original object and process each property value through the decorator function (the decorators method, that is, the default obtained through the getDefaultDecoratorFromObjectOptions method, is deepDecorator, so look at deepDecorator directly once)

export function extendObservableObjectWithProperties(
    target,
    properties, // Original object
    decorators,
    defaultDecorator
) {
    startBatch()
    const keys = ownKeys(properties)
    
    // Loop original object
    for (const key of keys) {
        const descriptor = Object.getOwnPropertyDescriptor(properties, key)!
        const decorator =
            decorators && key in decorators
                ? decorators[key]
                : descriptor.get
                ? computedDecorator
                : defaultDecorator
        const resultDescriptor = decorator!(target, key, descriptor, true) // Treated by decorator
        if (
            resultDescriptor // otherwise, assume already applied, due to `applyToInstance`
        )
            Object.defineProperty(target, key, resultDescriptor)
    }

    endBatch()
}

4.8 decorator

The decorator defaults to deep decorator. Let's see what it does.

export function createDecoratorForEnhancer(enhancer: IEnhancer<any>): IObservableDecorator {
    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
                // Call the target[$mobx].addObservableProp method
            asObservableObject(target).addObservableProp(propertyName, initialValue, enhancer)
        }
    )
    const res: any = decorator
    res.enhancer = enhancer
    return res
}

4.9 addObservableProp method

The target[$mobx].addObservableProp method is invoked in decorator.

addObservableProp(
    propName: PropertyKey,
    newValue,
    enhancer: IEnhancer<any> = this.defaultEnhancer
) {
    const { target } = this
    if (hasInterceptors(this)) {
        // Interception processing
        const change = interceptChange<IObjectWillChange>(this, {
            object: this.proxy || target,
            name: propName,
            type: "add",
            newValue
        })
        if (!change) return // When the interceptor returns null, you do not need to ignore this modification again.
        newValue = (change as any).newValue
    }
    // newValue converted to ObservableValue type
    const observable = new ObservableValue(
        newValue,
        enhancer,
        `${this.name}.${stringifyKey(propName)}`,
        false
    )
    this.values.set(propName, observable) // storage
    newValue = (observable as any).value 
    
    // The generateObservablePropConfig method returns the following descriptor
    // { ..., get() { return this[$mobx].read(propName)  }, set(v) { this[$mobx].write(propName, v) } }
    Object.defineProperty(target, propName, generateObservablePropConfig(propName)) // target generate propName attribute
    const notify = hasListeners(this)
    const change = {
          type: "add",
          object: this.proxy || this.target,
          name: propName,
          newValue
      }
    this.keysAtom.reportChanged() // this.keysAtom is an instance of Atom
}

addObservableProp method

  • 1) Call the ObservableValue class to convert newValue to observable value (remember that the ObservableValue call above called the observable.object method through enhancer. Now you can see that ObservableValue is called when looping the object's properties in the observable.object method. The object's properties are converted to observable values in this recursive way)
  • 2) Store the attribute key and observable in target[$mobx].values
  • 3) Add the attribute value of the original object to the target, and directly call this[mobx].read and this[mobx].write methods through get and set in the descriptor.
  • 4) Call the atomic class Atom's reportChanged and let the derivation that depends on this observable object perform the deployment operation again.

To sum up, the function of extendObservableObjectWithProperties is to cycle the original object, execute the above four steps, proxy the properties of the original object to the target, convert the values to observable values, and store them in target[$mobx].values.

4.10 read and write

read(key: PropertyKey) {
    return this.values.get(key)!.get()
}


 // observable.get method
 public get(): T {
    this.reportObserved() // reportObserved under Atom
    return this.dehanceValue(this.value)
}

The read method will find the corresponding observable object from this.values according to the attribute name, and then call the observable.get method to trigger reportObserved

write(key: PropertyKey, newValue) {
    const instance = this.target
    const observable = this.values.get(key)
    // intercept
    if (hasInterceptors(this)) {
        const change = interceptChange<IObjectWillChange>(this, {
            type: "update",
            object: this.proxy || instance,
            name: key,
            newValue
        })
        if (!change) return
        newValue = (change as any).newValue
    }
    newValue = (observable as any).prepareNewValue(newValue)
    if (newValue !== globalState.UNCHANGED) {
        (observable as ObservableValue<any>).setNewValue(newValue)
    }
}


// observable.prepareNewValue and observable.setNewValue methods
private prepareNewValue(newValue): T | IUNCHANGED {
    if (hasInterceptors(this)) {
        const change = interceptChange<IValueWillChange<T>>(this, {
            object: this,
            type: "update",
            newValue
        })
        if (!change) return globalState.UNCHANGED
        newValue = change.newValue
    }
    // apply modifier
    newValue = this.enhancer(newValue, this.value, this.name) // Call enhancer to convert to observable mode
    return this.equals(this.value, newValue) ? globalState.UNCHANGED : newValue
}

setNewValue(newValue: T) {
    const oldValue = this.value
    this.value = newValue
    this.reportChanged()
    if (hasListeners(this)) {
        notifyListeners(this, {
            type: "update",
            object: this,
            newValue,
            oldValue
        })
    }
}

write method

  • 1) Call the observable.prepareNewValue method to convert the new value
  • 2) Call observable.setNewValue to modify the value again
  • 3) Trigger the reportChanged method.

4.11 summary

var student = mobx.observable({
    name: 'Zhang San',
});

mobx uses the observable method to proxy the incoming object with target and assign it to student.
Therefore, the structure of student should be as follows

When calling student.name, get = > read = > observablevalue. Get = > reportobserved will be called
When modifying, set = > write = > observablevalue. Setnewvalue = > reportchanged

Now we can basically understand the relationship between observable and autoruan.

Keywords: Javascript Front-end mobx

Added by dyntryx on Sat, 04 Dec 2021 05:18:02 +0200