preface

We already know what an Observable does in Mobx source code parsing 1, but we still haven’t explained it clearly. In our Demo, we just increment and decrement bankuser. income in Button Click event. The innerText of incomeLabel is only processed in the mobx. Autorun method, so it’s easy to understand the mystery of this method. Let’s take a closer look at how this approach works.

Demo

A new branch of Autorun was created on Git, and minor changes were made to the Demo code, mainly to the Autorun method:

const incomeDisposer = mobx.autorun(() => {
    if (bankUser.income < 0) {
        bankUser.income = 0
        throw new Error('throw new error')
    } 
    incomeLabel.innerText = `Ivan Fan income is ${bankUser.income}`   
}, {
    name: 'income',
    delay: 2*1000,
    onError: (e) => {
        console.log(e)
    }
})
Copy the code

As you can see, we pass the autorun method a second argument, which is an Object:

{
    name: 'income',
    delay: 2*1000,
    onError: (e) => {
        console.log(e)
    }
Copy the code

We can guess from these three properties:

  1. Name should be a simple name for this
  2. Delay is a delay in execution
  3. OnError should be executed when autorun method execution error. The above is just based on the code guess, we will analyze this according to the source codeautorunMethods.

autorun

Autorun source code is as follows:

export function autorun(view, opts = EMPTY_OBJECT) {
    if(process.env.NODE_ENV ! = ="production") {
        invariant(typeof view === "function"."Autorun expects a function as first argument");
        invariant(isAction(view) === false."Autorun does not accept actions since actions are untrackable");
    }
    const name = (opts && opts.name) || view.name || "Autorun@"+ getNextId(); const runSync = ! opts.scheduler && ! opts.delay;let reaction;
    if (runSync) {
        // normal autorun
        reaction = new Reaction(name, function () {
            this.track(reactionRunner);
        }, opts.onError);
    }
    else {
        const scheduler = createSchedulerFromOptions(opts);
        // debounced autorun
        let isScheduled = false;
        reaction = new Reaction(name, () => {
            if(! isScheduled) { isScheduled =true;
                scheduler(() => {
                    isScheduled = false;
                    if(! reaction.isDisposed) reaction.track(reactionRunner); }); } }, opts.onError); }function reactionRunner() {
        view(reaction);
    }
    reaction.schedule();
    return reaction.getDisposer();
}
Copy the code

If you look at this method, you can see that it can pass two arguments:

  1. The view must be a function, where the business logic is to be executed.
  1. Opts, which is an optional argument, is an Object and can be passed four propertiesname.scheduler.delay.onErrorDelay and scheduler are two more important parameters because they determine whether to be synchronous or asynchronous.
  2. Look at the last second line of this methodreaction.schedule();, actually means that the autorun method has been called, will immediately execute a corresponding callback function

Synchronous processing

In the above sorting, it is found that if delay or scheduler value is passed, it enters the else logic branch, that is, the asynchronous processing branch. Now we first change the delay in demo: 2*1000, attribute to annotation, first analysis of synchronous processing logic (normal autorun normal Autorun)

Create an instance of reaction

We first create an instance object called Reaction, which passes two parameters: Name and a function, mounted on a property called onInvalidate, that will eventually execute the first argument to our autorun method, viwe, which is the business logic code we will execute:

        reaction = new Reaction(name, function () {
            this.track(reactionRunner);
        }, opts.onError);
Copy the code
    function reactionRunner() {
        view(reaction);
    }
Copy the code

Call the reaction.schedule() method

As you can see, when the Reaction object is instantiated, its schedule method is immediately executed, and then you simply return an object Reaction.getDisposer (), and the entire Autorun method is finished.

The autorun method looks simple, but why the view method can be executed as soon as its corresponding property changes lies in the Schedule method, so we should further analyze this method.

    schedule() {
        if(! this._isScheduled) { this._isScheduled =true; globalState.pendingReactions.push(this); runReactions(); }}Copy the code
  1. Set an identifier: _isScheduled = true to indicate that the current instance is scheduled
  2. globalState.pendingReactions.push(this);Puts the current instance in a global arrayglobalState.pendingReactions
  3. Run the runReactions method.

RunReactions method (Run all reactions)

const MAX_REACTION_ITERATIONS = 100;
let reactionScheduler = f => f();
export function runReactions() {
    if (globalState.inBatch > 0 || globalState.isRunningReactions)
        return;
    reactionScheduler(runReactionsHelper);
}
function runReactionsHelper() {
    globalState.isRunningReactions = true;
    const allReactions = globalState.pendingReactions;
    let iterations = 0;   
    while (allReactions.length > 0) {
        if (++iterations === MAX_REACTION_ITERATIONS) {
            allReactions.splice(0); // clear reactions
        }
        let remainingReactions = allReactions.splice(0);
        for (let i = 0, l = remainingReactions.length; i < l; i++)
            remainingReactions[i].runReaction();
    }
    globalState.isRunningReactions = false;
}
Copy the code
  1. Determine global variablesglobalState.inBatch > 0 || globalState.isRunningReactionsIs there a reaction going on?
  2. Run runshelper ()
  3. Set the globalState. IsRunningReactions = true;
  4. Get the reaction of all waiting,const allReactions = globalState.pendingReactions;(we are inscheduleMethod analysis, in which each reaction instance is placed in the globalState array)
  5. Go through all the reactions in wait and runrunReactionMethods (remainingReactions[i].runReaction();)
  6. The final will beglobalState.isRunningReactions = false;That way, there’s only one at a timeautorunIn operation, to ensure the correctness of the data

We analyzed the basic flow, and the final implementation was in the Reaction instance method runReaction method, which we are now going to analyze.

RunReaction method (which actually implements the business logic in Autorun)

    runReaction() {
        if(! this.isDisposed) { startBatch(); this._isScheduled =false;
            if (shouldCompute(this)) {
                this._isTrackPending = true;
                try {
                    this.onInvalidate();
                    if(this._isTrackPending && isSpyEnabled() && process.env.NODE_ENV ! = ="production") {
                        spyReport({
                            name: this.name,
                            type: "scheduled-reaction"}); } } catch (e) { this.reportExceptionInDerivation(e); } } endBatch(); }}Copy the code
  1. startBatch();It’s just set upglobalState.inBatch++;
  2. this.onInvalidate();The key is this method, this method is instantiationReactionObject, whose final code looks like this:
  reaction = new Reaction(name, function () {
        this.track(reactionRunner);
    }, opts.onError);
Copy the code
    function reactionRunner() {
        view(reaction);
    }
Copy the code

So this.onInvalidate is:

function () {
     this.track(reactionRunner);
}
Copy the code

How does it relate to objects handled by an Observable?

The basic logic of autorun has been analyzed above, and we can write this. Track (reactionRunner); To check function’s Call Stack, set a breakpoint.

derivation.js

  1. Derivation is the Reaction instance created by the Autorun approach
  1. F, the callback function of Autorun, is the Derivation onInvalidate attribute

Result = f.call(context); This is obviously where the autorun method callback is executed.

We see in this way will the current derivation assigned to the globalState. TrackingDerivation = derivation; , this value is called elsewhere. Let’s go back and see what autorun’s callback is:

const incomeDisposer = autorun((reaction) => {
    incomeLabel.innerText = `${bankUser.name} income is ${bankUser.income}`})Copy the code

Here, we call bankuser. name and bankuser. income. BankUser is an object processed by an Observable. So if we read any of his properties, we’re going to type the get method of the interceptor, so let’s look at what the get method does.

The Proxy the get method

The get method looks like this:

    get(target, name) {
        if (name === $mobx || name === "constructor" || name === mobxDidRunLazyInitializersSymbol)
            return target[name];
        const adm = getAdm(target);
        const observable = adm.values.get(name);
        if (observable instanceof Atom) {
            return observable.get();
        }
        if (typeof name === "string")
            adm.has(name);
        return target[name];
    }
Copy the code

ObservableValue is an ObservableValue type, and ObservableValue inherits from Atom, so the code branches off as follows: ObservableValue (ObservableValue)

    if (observable instanceof Atom) {
            return observable.get();
        }
Copy the code

Let’s move on to the corresponding GET method

    get() {
        this.reportObserved();
        return this.dehanceValue(this.value);
    }
Copy the code

There is a key method: this.reportobserved (); As the name implies, I’m reporting that I’m being observed, associating an Observable with the Autorun method, so we can follow up on this method.

With breakpoints, we see that the reportObserved method of Observable. js is eventually called.

export function reportObserved(observable) {
    const derivation = globalState.trackingDerivation;
    if(derivation ! == null) {if(derivation.runId ! == observable.lastAccessedBy) { observable.lastAccessedBy = derivation.runId; derivation.newObserving[derivation.unboundDepsCount++] = observable;if(! observable.isBeingObserved) { observable.isBeingObserved =true; observable.onBecomeObserved(); }}return true;
    }
    else if (observable.observers.size === 0 && globalState.inBatch > 0) {
        queueForUnobservation(observable);
    }
    return false;
}
Copy the code
  1. Parameter: Observable is an ObservableValue object. In the analysis in Chapter 1, we know that every property processed by observable is processed by this type of object, so this object is the corresponding property.
  2. The second lineconst derivation = globalState.trackingDerivation;This line of code is fairly easy to understand, but the source of the value is important, as we found abovederivation.jsIn the trackDerivedFunction method, it is found that the value is assigned to itglobalState.trackingDerivation = derivation;. And the corresponding valuederivationThat’s the correspondingautorunTo create theReactionobject
  3. derivation.newObserving[derivation.unboundDepsCount++] = observable;This line is crucial to actually associating the Observable property with the Autorun method.

In our autorun method calls the two properties, so in carrying out twice after the get method, the corresponding globalState. TrackingDerivation values as shown in the figure below:

In the newObserving property, there are two values, two values, which means that the current autorun method will listen to the two properties, we will resolve the next, how to deal with the newObserving array

We continue to analyze the trackDerivedFunction method

export function trackDerivedFunction(derivation, f, context) {
    changeDependenciesStateTo0(derivation);
    derivation.newObserving = new Array(derivation.observing.length + 100);
    derivation.unboundDepsCount = 0;
    derivation.runId = ++globalState.runId;
    const prevTracking = globalState.trackingDerivation;
    globalState.trackingDerivation = derivation;
    let result;
    if (globalState.disableErrorBoundaries === true) {
        result = f.call(context);
    }
    else {
        try {
            result = f.call(context);
        }
        catch (e) {
            result = new CaughtException(e);
        }
    }
    globalState.trackingDerivation = prevTracking;
    bindDependencies(derivation);
    return result;
}
Copy the code

Result = f.call(context); For this step, we now consider: bindDependencies(Derivation); methods

BindDependencies method

The derivation parameter is derivation. When executing the GET method for each attribute, two records have been added to the Derivatio’s newObserving property, as shown:

Next, we analyze the bindDependencies method in depth and find that it traverses newObserving as follows

    while (i0--) {
        const dep = observing[i0];
        if(dep.diffValue === 1) { dep.diffValue = 0; addObserver(dep, derivation); }}Copy the code

addObserver(dep, derivation); If the method name is null, the method name is null. If the method name is null, it is null.

export function addObserver(observable, node) {
    observable.observers.add(node);
    if (observable.lowestObserverState > node.dependenciesState)
        observable.lowestObserverState = node.dependenciesState;
}
Copy the code

Observable is the ObservableValue for each of our attributes, and there is a Set attribute, observers. Node is the Reaction object created by our autorun method

observable.observers.add(node); That is, each property holds its corresponding observer.

It ends up processing the Observable object as follows (adding a value to the pau in step 3) :

conclusion

  1. Running the Autorun method produces an object of type Reaction
  2. Run the autorun callback (parameter), which references observable properties and fires the corresponding Proxy Get method
  3. In the get method, the ObservableValue object, decorated with the corresponding property, is saved into the Reaction newObserving array in the first point. (If two Observable properties are referenced in the Autorun callback, NewObserving will have two records.)
  4. Once the callback is done, a bindDependencies method is called, which iterates through the newObserving array, saving the Reaction objects generated in the first point into the observers property of the corresponding ObservableValue object, for each property. If a property is referenced by multiple Autorun methods, the observers attribute stores all Reaction objects (in effect, all listeners in observers mode).
  5. Will eventuallyobservableThe object is processed to the object shown below
  6. So the autorun function, in effect, is imposing a value, namely, observers, on observers, in the third point in the diagram.

Todo

We already know that the Observable and Autorun methods are associated. We will continue to analyze how to trigger the Autorun callback when changing the observable property value. My guess is that, for starters, Proxy set will be fired, and that set will iterate through the Reaction onInvalidate method in Observers, so let’s look at this in detail.