A, introducing

We use Mobx a lot when we write code, so let’s take a look at how mobx works to solve some of these weird problems. (Ps: If you haven’t mentioned any problems in this article, please fill in the comments.

Let’s take a look at an example of a real project, as shown in the following figure. This is a very common scenario where the page is laid out left and right, the left side is a sidebar that can be expanded and folded, the top right side is a chart and the bottom side is a table.

After the menu on the left is expanded, a scroll bar appears in the chart in the content area on the right because the width is the same as before the menu was expanded. The chart also exceeds the display area of the viewport. How do people deal with this problem on a daily basis?

Xiong’s solution is to trigger the chart resize method in the right content area by listening for changes in the unfolding state of the sidebar

@observable viewState = { isSidebarCollapsed: false };

this.disposer = reaction(
    () = > viewState.isSidebarCollapsed,
    () = >{ chartResize(); });Copy the code

Have you noticed that Bear has written a Disposer, which will call to remove the reaction when the component dismounts

Optimizations suggest that reactions should always be cleaned up, or they will only be garbage collected if the objects they observe are garbage collected. Reactions like Autorun are much trickier because they may observe many different Observables, and the reaction will stay scoped as long as one of them remains scoped, This means that all other Observables it uses remain active for future recalculations. — Mobx official documentation (Always Clean up – Reaction)

Because the source code is quite long, we will attach the core logic of the source code. The source code will be attached in the link

2. Mobx operation mechanism

2.1 observables

Let’s take a look at the observable source code and see what it does

Observable

var observable: IObservableFactory = assign(createObservable, observableFactories);
Copy the code

ObservableFactories (observableFactories) an Observable consists of two parts: the createObservable function and the observableFactories object. Let’s look at these two parts separately.

createObservable

function createObservable(v: any, arg2? : any, arg3? : any) {
  // @observable someProp;
  if (isStringish(arg2)) {
    storeAnnotation(v, arg2, observableAnnotation)
    return
  }
 
  // already observable - ignore
  if (isObservable(v)) {
    return v
  }
 
  // plain object
  if (isPlainObject(v)) {
    return observable.object(v, arg2, arg3)
  }
 
  // Array
  if (Array.isArray(v)) {
    return observable.array(v, arg2)
  }
 
  // Map
  if (isES6Map(v)) {
    return observable.map(v, arg2)
  }
 
  // Set
  if (isES6Set(v)) {
    return observable.set(v, arg2)
  }
 
  // other object - ignore
  if (typeof v === 'object'&& v ! = =null) {
    return v
  }
 
  // anything else
  return observable.box(v, arg2)
}
Copy the code

CreateObservable is a forwarding function that simply forwards incoming objects to different transform functions based on the object type.

【 source 】observableFactories

const observableFactories: IObservableFactory = {
  box(){},
  array(){},
  map(){},
  set(){}, object<T = any>(props: T, decorators? : AnnotationsMap<T, never>, options? : CreateObservableOptions): T {return extendObservable(
      globalState.useProxies === false|| options? .proxy ===false
        ? asObservableObject({}, options)
        : asDynamicObservableObject({}, options),
      props,
      decorators
    )
  },
  ref: createDecoratorAnnotation(observableRefAnnotation),
  shallow: createDecoratorAnnotation(observableShallowAnnotation),
  deep: observableDecoratorAnnotation,
  struct: createDecoratorAnnotation(observableStructAnnotation)
} as any
Copy the code

ObservableFactories consists of two observableFactories. One observableFactories, like createObservable, is a conversion function of various types, such as Box, array, Map, set, and Object, and the other is a collection of properties.

  • ref: There is no need to observe attribute changes (attributes are read-only), and ref is used when references are frequently changed. The underlying processing disables automatic Observable transformations and just creates an Observable reference.
  • shallow: Observe only layer 1 data. There is no recursive transformation for the underlying processing.
  • deep: Indicates whether to enable in-depth observation.
  • structThe reaction is triggered every time an object is updated, but sometimes only the reference updates the actual property content, which is why structs exist. Structs do deep comparisons based on properties.

ObservableFactories (observableFactories, observableFactories, observableFactories, observableFactories) We can see that the Observable. object method actually calls the extendObservable method. Let’s scroll down to see what extendObservable does.

extendObservable

function extendObservable<A extends Object.B extends Object> (target: A, properties: B, annotations? : AnnotationsMap
       
        , options? : CreateObservableOptions
       ,>) :A & B {
  // Pull descriptors first, so we don't have to deal with props added by administration ($mobx)
  const descriptors = getOwnPropertyDescriptors(properties)
 
  const adm: ObservableObjectAdministration = asObservableObject(target, options)[$mobx]
  startBatch()
  try {
    ownKeys(descriptors).forEach(key= > {
      adm.extend_(
        key,
        descriptors[key as any],
        // must pass "undefined" for { key: undefined }! annotations ?true : key in annotations ? annotations[key] : true)})}finally {
    endBatch()
  }
  return target as any
}
Copy the code

Following the source call let’s take a look at what extendObservable does. It does the following:

1. Call the asObservableObject method to mount the $mobx property to target

2. Iterate through the target property values and have each property modified by the decorator and remounted to target. We follow the code to see that there is a call chain like this:

The $mobx. Extend_ – defineProperty_ – defineObservableProperty_ – getCachedObservablePropDescriptor

As shown below is getCachedObservablePropDescriptor source, we find it to intercept the target each attribute of read operation, and operation map to mobx properties above, this means that after the tagert read will be mapped to mobx property, This means that future reads of Tagert are mapped to mobx properties, which means that future reads of Tagert are mapped to MOBx.

[source] getCachedObservablePropDescriptor

function getCachedObservablePropDescriptor(key) {
  return (
    descriptorCache[key] ||
      (descriptorCache[key] = {
        get() {
          return this[$mobx].getObservablePropValue_(key)
        },
        set(value) {
          return this[$mobx].setObservablePropValue_(key, value)
        }
      })
  )
}
Copy the code

So let’s go ahead and see what getObservablePropValue and setObservablePropValue_ do, That is, what does an Observable do when it reads an object wrapped in an Observable?

GetObservablePropValue_ → get_ → has_ → get → reportObserved → queueForUnobservation

The focus of this call chain is the reportObserved function, so let’s focus on its source ~

reportObserved

function reportObserved(observable: IObservable) :boolean {
    checkIfStateReadsAreAllowed(observable)
 
    const derivation = globalState.trackingDerivation
    if(derivation ! = =null) {
        if(derivation.runId_ ! == observable.lastAccessedBy_) { observable.lastAccessedBy_ = derivation.runId_// Tried storing newObserving, or observing, or both as Set, but performance didn't come close...derivation.newObserving_! [derivation.unboundDepsCount_++] = observableif(! observable.isBeingObserved_ && globalState.trackingContext) { observable.isBeingObserved_ =true
                observable.onBO()
            }
        }
        return true
    } else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
        queueForUnobservation(observable)
    }
 
    return false
}
Copy the code

As can be seen from the read source code, step by step the read triggers reportObserved, and finally wraps the properties of the current object into an ObservableValue object added to the global variable isPendingUnobservation_.

An Observable reads an observable that does a dependency collection in the observable environment. We have an idea of what write operations do in the observable environment. Click the corresponding function to jump to Github repository for specific source code:

SetObservablePropValue_ → prepareNewValue_ → interceptChange → setNewValue_ → reportChanged + notifyListeners

When writing values to Observable, intercepts before writing values and performs some processing in the interceptor. Notifies all observers that the value is updated after the value is written. Emmmm, this is typical observer mode

To summarize, the Observable source code can be found along the way:

  1. Observable first uses policy mode to forward objects based on their type
  2. The proxy mode is used to broker operations on the original object by mounting the $mobx property on the object after forwarding to the corresponding transform function.
  3. Dependency collection is performed for proxy reads and observer notification is performed for proxy writes. Observer mode is used here
  4. Two callbacks (AOP in this case) are inserted before and after the value is written, like an Axios intercept and like reaction after the value is written in response to the new value.

2.2 reaction

Emmmm, after seeing what the Reaction does, let’s see what the Reaction does

【 source 】reaction: Emmmm, it is too long to post the bear

Following the reaction source, we can find a call stack that looks like this:

Reaction. Schedule_ → runReactions → runshelper → runReaction_

When we read the implementation of Runreaction, it will do the following:

  • StartBatch and endBatch are used to start and end a transaction to batch reaction and avoid unnecessary recalculation (a reference to database transactions).
  • ShouldCompute is used to determine whether to continue, and if so onInvalidate_

Follow onInvalidate_ to find a call stack like this:

OnInvalidate_ → reactionRunner → track → trackDerivedFunction → bindDependencies

Based on this call stack, we can see that onInvalidate_ mainly does the following things:

  • Tracking task (track) : open a new layer, and the current reaction mount to global variables globalState. TrackingContext
  • TrackDerivedFunction: Performs reaction’s callback and updates the newObserving_ property for updating dependencies later
  • Update Dependencies: Update the Implementation of the Derivation dependencies with newObserving_ to update the Observing_ property in the Current Derivation runtime environment

Insert a small expansion of knowledge ~


In dependent update, two arrays of observing_ and newObserving_ are involved. The time complexity of the algorithm to judge by normal double-layer loop is O(n^2), but mobX reduces the complexity to O(n) with the help of the triple loop + diffValue attribute. This understanding is referenced in section 2.2.3.2 of Reference 1.

The initial value of diffValue is 0. If the diffValue is 0, the value will be changed to 1. If the diffValue is 1, it indicates the repetition and can be deleted directly. The result of the first traversal is as follows:

After this walk, the diffValue of all the latest dependencies is 1 and all duplicate dependencies are removed.

2. For the second time through observing, the diffValue is 0, indicating that it is not in the updated dependency, you can call removeObserver to get rid of it directly. If the diffValue is 1, it will change to 0.

After this walk, all the old observing dependencies are removed, and the diffValue of the remaining objects is 0.

3. Loop through newObserving for the third time. If the diffValue is 1, it indicates that the new dependency is generated.


Emmmm, we have a simple idea of how observable and Reaction are implemented, but we still don’t know how observable changes trigger reaction. Let’s see how Mobx connects the two

Observable writes reportChanged, opens a new transaction within reportChanged and calls propagateChanged, PropagateChanged traverses the observables observer list (i.e. done observables read operations of reaction, when reading was added to the observables observer queue). And then execute the onBecomeStale_ method for each observer, and the onBecomeStale_ method directly calls the reaction schedule_ method, which is to take the reaction process that we talked about above and put it back to the side ~ EMMM, The observable and reaction processes are strung together to pull ~

Third, summary

Finally, let’s review the main content of this article. This article starts with the actual project sidebar opening up the right side of the chart area where content will not be resized, then presents the Mobx solution, and follows the code in the solution to read the source code. See what mobx observables and Reaction do.

observable

Wrap the incoming data into an ObservableValue object and proxy its read and write operations:

  • The reportObserved method is called at read time for dependency collection

ReportObserved puts observables into the Derivation newObserving_ queue, Update dependencies with addObservable and removeObservale when implemented into bindDependencies AddObservable and removeObservale update the Observers_ property of an Observable.)

  • Call the reportChanged method for notification at write time

(reportChanged calls propagateChanged, passes through observable observers -observers_ in propagateChanged, and then makes them execute their respective onBecomeStale_ methods, To go from the onBecomeStale_ method is to take the reaction process all over again.)

reaction

Check whether it is necessary to execute by shouldCompute, shouldCompute mainly do performance optimization, see the specific implementation principle of this article. Perform the following tasks if necessary:

  • Tracking task (track) : open a new layer, and the current reaction mount to global variables globalState. TrackingContext
  • TrackDerivedFunction: Performs reaction’s callback and updates the newObserving_ property for updating dependencies later
  • Update Dependencies: Update the Implementation of the Derivation dependencies with newObserving_ to update the Observing_ property in the Current Derivation runtime environment

Emmmm, can you tell us from the mobx source point of view why the original code can resize the right side of the chart when the sidebar is expanded and closed?

Four, small test

  1. What is the output of the following two questions, and why?

  1. We didn’t talk about autorun. Can you guess the relationship between autorun and reaction?

  2. Why is mobx’s strict mode that changes to mobx values must be wrapped in the action? Guess what the difference is between an action and a runInAction?

  3. Will the title of the SomeContainer component be updated in the following code? If not, please explain why not, and how to update it ~

Five, debugging skills

Congratulations on finishing all the questions and two tips for mobx debugging

  1. Track is used to monitor which Observable changes cause the page changes. Refer to the document for specific operations, and the results are as follows:

  1. Mobx-react-devtools, a developer tool that comes with MobX, tracks application rendering behavior and data dependencies.

Finally, since you’ve bothered to read the entire article, reward yourself for asking any questions you want on the document

Vi. Reference materials

  1. Read the MobX source code series with stories
  2. Mobx making source
  3. Mobx official documentation
  4. Mobx6 core source code parsing (5) : Reaction and transactions