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:
- Name should be a simple name for this
- Delay is a delay in execution
- 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 code
autorun
Methods.
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:
- The view must be a function, where the business logic is to be executed.
- Opts, which is an optional argument, is an Object and can be passed four properties
name
.scheduler
.delay
.onError
Delay and scheduler are two more important parameters because they determine whether to be synchronous or asynchronous.- Look at the last second line of this method
reaction.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
- Set an identifier: _isScheduled = true to indicate that the current instance is scheduled
globalState.pendingReactions.push(this);
Puts the current instance in a global arrayglobalState.pendingReactions
- 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
- Determine global variables
globalState.inBatch > 0 || globalState.isRunningReactions
Is there a reaction going on? - Run runshelper ()
- Set the globalState. IsRunningReactions = true;
- Get the reaction of all waiting,
const allReactions = globalState.pendingReactions;
(we are inschedule
Method analysis, in which each reaction instance is placed in the globalState array) - Go through all the reactions in wait and run
runReaction
Methods (remainingReactions[i].runReaction();
) - The final will be
globalState.isRunningReactions = false;
That way, there’s only one at a timeautorun
In 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
startBatch();
It’s just set upglobalState.inBatch++;
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
- Derivation is the Reaction instance created by the Autorun approach
- 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
- 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.
- The second line
const 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 valuederivation
That’s the correspondingautorunTo create theReactionobject 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
- Running the Autorun method produces an object of type Reaction
- Run the autorun callback (parameter), which references observable properties and fires the corresponding Proxy Get method
- 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.)
- 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).
- Will eventuallyobservableThe object is processed to the object shown below
- 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.