3
头图

proxy

interception method

There are various interception APIs exposed by Mobx, which can be broadly divided into decorator-based and observable-based method calls.

decorator
For students who don't know much about decorators, you can see my previous article: Exploration of the principle of decorators, and the behavior of decorators is obtained by analyzing the translated ES code.

Since the decorator is still in the proposal in ES and the behavior of the decorator at each stage is inconsistent , the writing method of the decorator has been eliminated since mobx 6.x (it can also be manually opened). The source code analysis of this article is based on the mobx 5.x version ( The principle is exactly the same as 6.x), at this time the decorator is based on babel

 {
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
  ]
}

Implemented in this configuration, using the syntax and behavior of the legacy (stage 1) decorator.

 import { observable } from 'mobx';

class A {
  @observable a = 1;
}

Here @observable the behavior of the decorator is actually to hang a getter setter on the prototype that instantiates to A.

 {
            configurable: true,
            enumerable: enumerable,
            get: function() {
                initializeInstance(this)
                return this[prop]
            },
            set: function(value) {
                initializeInstance(this)
                this[prop] = value
            }
        }

When instantiated, it will execute instance.a = 1 assignment operation, trigger the setter, and go to the logic of mobx processing class instance:

  1. Hang the object management class (adm) on the instance
  2. Recursively wrap value and collect in adm
  3. hang a getter setter for the key (a) on the instance
 {
            configurable: true,
            enumerable: true,
            get() {
                // 收集依赖
                return this[$mobx].read(propName)
            },
            set(v) {
                // 触发更新
                this[$mobx].write(propName, v)
            }
}

Macroscopically speaking, after accessing the decorated properties, it will go to this[$mobx].read(propName) to collect side effects, and when the properties change, it will go to this[$mobx].write(propName, v) to execute side effects.

observable imperative call

An imperative call looks like this:

 const xxx = observable(xxx) || observable.xx(xxx);

There must be a return value. When we operate the return value, we will do the behavior of collecting | performing side effects.

The following will analyze each type of interception one by one.

Let’s first explain a concept. In Mobx, all values that need to be observed, except for arrays and Sets, will be packaged by the ObservableValue class (for convenience, the instance will be referred to as OV later), and the work done is:

(1) Use enhancer to process value

(2) Manage the packaged value in (1) (read and write, collect dependencies, etc.)

There are many types of enhancers. If the user does not make additional configuration, Mobx uses deepEnhancer for packaging each value by default, which is actually the operation of recursively making an observable imperative call to this value.

primitive value

For primitive type value, Mobx only supports the use of observable.box(val) this API for interception, in fact, an OV is returned internally.

If read and write use OV to expose get set API respectively.

object

Use observable.object(val) to intercept, and three things are done internally:

  1. Create a new empty object { }, and attach the object management class (adm) to { }
  2. Proxy intercepts { } and saves the proxy object in adm.proxy
  3. Iterate over the keys of val:

    1. Recursively wrap value and collect OV in adm
    2. Hang the getter setter for each key in { } (the same as the getter setter hanging on the decorator)
  4. return proxy object

Proxy handlers are:

 // 暂时只关注读写
{
    get(target: IIsObservableObject, name: PropertyKey) {
        // ... 忽略暂时无关代码
        const adm = getAdm(target)
        // 拿到 OV
        const observable = adm.values.get(name)
        if (observable instanceof Atom) {
              // 此处等同于调用 adm.read(propName)
            const result = (observable as any).get() 
            // ...
            return result
        }
        // ... 
    },
    set(target: IIsObservableObject, name: PropertyKey, value: any) {
        if (!isPropertyKey(name)) return false
        set(target, name, value)
          // 这个 set 方法针对对象最终执行如下
          // // ...
        // if (isObservableObject(obj)) {
        //  const adm = ((obj as any) as IIsObservableObject)[$mobx]
        //  // 拿到 OV
        //  const existingObservable = adm.values.get(key)
        //  if (existingObservable) {
        //      adm.write(key, value)
        //  }
        //  // ...
          // }
        // //...
        return true
    },
}

As can be seen from the above, the read processing here is ultimately the same as the read and write processing of the object attributes decorated by the decorator.

array

Use observable.array(val) to intercept, and do five things internally:

  1. Initialize the array management class (adm), mount it on [ ], and mount [ ] on adm.values
  2. Proxy intercepts [ ] and mounts the proxy object in adm.proxy
  3. Iterate over val, recursively wrapping each element
  4. Update 3 one by one OV in adm.values
  5. return proxy object

Handler is as follows:

 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)) {
            // arrayExtensions 捕获数组方法
            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
    },

Taking reading as an example, it is like this in arrayExtensions:

 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]
                // ...
                // 新的被 enhancer 包装过的 value 
                newValue = adm.enhancer(newValue, oldValue)
                const changed = newValue !== oldValue
                if (changed) {
                    values[index] = newValue // 改变 adm 里收集的旧 value
                    // 通知更新
                    adm.notifyArrayChildUpdate(index, newValue, oldValue)
                }
            } 
              // ...
        }

As I said before, the array will not be wrapped by ObservableValue, because in its management class, the work of ObservableValue has been implemented, that is:

(1) Use enhancer to process value

(2) Manage the packaged value in (1) (read and write, collect dependencies, etc.)

In fact, the core of the operations in arrayExtensions is to collect dependencies and trigger updates.

map

Use observable.map(val) to intercept, and three things are done internally:

  1. Initialize the map management class
  2. Traverse the val, wrap the value in ObservableValue one by one, and collect it on the this._data of the management class
  3. Returns an instance of the map management class

The returned instance has API methods of Map, take read and write as an example:

 get(key: K): V | undefined {
              // this._data.get(key)!.get() 等同于调用对象 adm 的 adm.read(propName)
              // 收集依赖
        if (this.has(key)) return this.dehanceValue(this._data.get(key)!.get())
        return this.dehanceValue(undefined)
    }

set(key: K, value: V) {
        const hasKey = this._has(key)
        // ...
        if (hasKey) {
            this._updateValue(key, value)
        } else {
            this._addValue(key, value)
        }
        return this
    }

_updateValue(key: K, newValue: V | undefined) {
              // 拿到 ObservableV
        const observable = this._data.get(key)!
        // enhancer 新 value,然后对比旧 value 是否相等
        newValue = (observable as any).prepareNewValue(newValue) as V
        if (newValue !== globalState.UNCHANGED) {
            // ...
            // 更新并通知更新
            observable.setNewValue(newValue as V)
            // ...
        }
    }

set

Use observable.set(val) for interception, which does three things internally:

  1. Initialize the set management class
  2. Traverse the val, pack the value one by one enhancer, and collect it on the this._data of the management class
  3. Returns the set management class instance

Like Map, the returned set management class also has API related to Set, take all values and add as an example:

 add(value: T) {
        // ... 
        if (!this.has(value)) {
            transaction(() => {
                // 往本地缓存的 _data 里新增 enhancer 后的 value
                this._data.add(this.enhancer(value, undefined))
                // 通知依赖更新
                this._atom.reportChanged()
            })
            // ...
        }

        return this
    }

keys(): IterableIterator<T> {
        return this.values()
    }

values(): IterableIterator<T> {
        // 通知收集依赖
        this._atom.reportObserved()
        const self = this
        let nextIndex = 0
        const observableValues = Array.from(this._data.values())
        // 在 for of 中挨个读 _data 的值
        return makeIterable<T>({
            next() {
                return nextIndex < observableValues.length
                    ? { value: self.dehanceValue(observableValues[nextIndex++]), done: false }
                    : { done: true }
            }
        } as any)
    }

As can be seen from the above, in fact, for different data structures, the core of processing is to intercept the getter setter or related API of the observed object, so as to achieve the purpose of collecting dependencies when reading, and notifying dependencies to update when they change.

Derivation

Mobx has the concept of derivation, similar to observers. The product of Proxy is used in Derivation, and Derivation (observer) is derived (notified) whenever the product changes.

Some concepts:

transaction

Referring to the concept of database transactions, transactions in Mobx are used to batch the execution of React (Derivation Manager) to avoid unnecessary recalculation. Mobx's transaction implementation is relatively simple, using startBatch and endBatch to start and end a transaction:

 function startBatch() {
  // 通过一个全局的变量 inBatch 标识事务嵌套的层级
  globalState.inBatch++
}

function endBatch() {
  // 最外层事务结束时,才开始执行重新计算
  if (--globalState.inBatch === 0) {
    // 执行所有 Reaction
    runReactions()
    // 处理不再被观察的 ObservableV
    const list = globalState.pendingUnobservations
    for (let i = 0; i < list.length; i++) {
      const observable = list[i]
      observable.isPendingUnobservation = false
      if (observable.observers.length === 0) {
          observable.onBecomeUnobserved()
      }
    }
    globalState.pendingUnobservations = []
  }
}

For example, an Action starts and ends with the start and end of a transaction, ensuring that changes to the state in the Action (possibly multiple times) only trigger a re-execution of the Action.

 function startAction() {
  // ...
  startBatch()
  // ...
}
function endAction() {
  // ...
  endBatch()
  // ...
}
Reaction

Reaction is the manager of Derivation and implements the interface of Derivation:

 interface IDerivation extends IDepTreeNode {
  // 依赖数组
  observing: IObservable[]
  // 每次执行收集到的新依赖数组
  newObserving: null | IObservable[]
  // 依赖的状态
  dependenciesState: IDerivationState
  // 每次执行都会有一个 uuid,配合 Observable 的 lastAccessedBy 属性做简单的性能优化
  runId: number
  // 执行时新收集的未绑定依赖数量
  unboundDepsCount: number
  // 依赖过期时执行
  onBecomeStale()
}
Derivation state machine

Derivation marks the four states of dependencies through the dependenciesState attribute:

  1. NOT_TRACKING: Before execution, or outside the transaction, or not observed (calculated value), the state. At this point Derivation does not have any information about the dependency tree. enum value -1
  2. UP_TO_DATE: Indicates that all dependencies are up-to-date and will not be recalculated in this state. enum value 0
  3. POSSIBLY_STALE: A state that is only available for calculated values, indicating that the deep dependency has changed, but it cannot be determined whether the shallow dependency has changed. It will be checked before recalculation. enum value 1
  4. STALE: expired state, that is, the shallow dependency has changed, and Derivation needs to be recalculated. enum value 2

Any status tends to UP_TO_DATE.

------------------------- 2 ------------------------ -STALE

 

-------------↓--- 1 ------------------ POSSIBLY_STALE

 ↓        ↓

------- 0 -------- UP_TO_DATE

 

-1--- NOT_TRACKING

The rules of the state machine are:

  1. Initially, it is NOT_TRACKING, and after binding dependencies and derivations, it collectively becomes U_T_D.
    Unbundling falls back to NOT_TRACKING.
  2. When a collected dependency changes, its own dependency status and Derivation (after onBecomeStale) become STALE.
    After Derivation is reprocessed, both its own and collected dependencies become U_T_D.
  3. After the computed property is calculated (including the first time), the derived state and the collected dependency state all become U_T_D. (Conforms to 2. After the second sentence Derivation is reprocessed, both its own and collected dependencies become U_T_D) After being bound for the first time, it conforms to 1.

    If the state of a dependency A collected by the computed property changes, set the A state and computed property derived state (after onBecomeStale) to STALE (complies with the first sentence of 2), and set the computed property dependency state and computed property derived Derivation to P_STALE (the difference). After the computed property is recalculated, its derived state and all the collected dependency states are changed to U_T_D (in line with the second sentence of 2). If the calculation result does not change, the computed property dependency state and the derived Derivation of the computed property are changed back to U_T_D. If there is a change, change the Derivation derived from the computed property to STALE, then reprocess the Derivation derived from the computed property, and change the state of it and its collected dependencies (including computed properties as dependencies) to U_P_D.

The following takes AutoRun, Computed Value, and React Render as examples to analyze the source code of Derivation.

AutoRun

process

The usual usage is:

 autorun(cb)

First, a React will be initialized for the Derivation of AutoRun for management:

 function autorun(
    view: (r: IReactionPublic) => any, // cb
    opts: IAutorunOptions = EMPTY_OBJECT // 忽略
): IReactionDisposer {

    const name: string = (opts && opts.name) || (view as any).name || "Autorun@" + getNextId()
    const runSync = !opts.scheduler && !opts.delay
    let reaction: Reaction

    if (runSync) {
        // normal autorun
        // 用一个 reaction 来管理该 autorun
        reaction = new Reaction(
            name,
            function(this: Reaction) {
                this.track(reactionRunner)
            },
            opts.onError,
            opts.requiresObservable
        )
    }
    function reactionRunner() {
        view(reaction)
    }
        // 将该 reaction 列入计划表
    reaction.schedule()
    // 返回销毁方法
    return reaction.getDisposer()
}

The schedule table maintains a global array, and the Reactions stored in it are the Reactions that need to be executed in the batch.

 schedule() {
        // Reaction 已经在重新计算的计划表内,直接返回
        if (!this._isScheduled) {
            this._isScheduled = true
            // 该 Reaction 加入全局的待重新计算数组中
            globalState.pendingReactions.push(this)
            runReactions()
        }
    }
 export function runReactions() {
    // 惰性更新,若此时处于事务中,inBatch > 0,会直接返回
    if (globalState.inBatch > 0 || globalState.isRunningReactions) return
    reactionScheduler(runReactionsHelper)
}
 function runReactionsHelper() {
    globalState.isRunningReactions = true
    // 取出当前批次收集的所有 Reaction
    const allReactions = globalState.pendingReactions
    let iterations = 0

    // 当执行 Reaction 时,可能触发新的 Reaction(Reaction 内允许设置 Observable的值),加入到 pendingReactions 中
    while (allReactions.length > 0) {
        // 设定 Reaction 计算的最大迭代次数,避免造成死循环
        if (++iterations === MAX_REACTION_ITERATIONS) {
            // ... error
            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
}

The next step is to execute the logic of React. The main purpose is to run cb and collect the OV used.

 runReaction() {
        if (!this.isDisposed) {
            // 开启一个事务处理,因为运行 cb 的过程中可能会再加 Reaction 到计划表(比如依赖更新)
            startBatch()
            this._isScheduled = false
            // 判断 Reaction 收集的依赖状态
            // 如状态机所示,只有在 NO_TRACKING | STALE | 判断 COMPUTED 值变化时才会执行 Reaction 
            if (shouldCompute(this)) {
                this._isTrackPending = true

                try {
                      // 处理 cb 
                    this.onInvalidate()
                    // ...
                } catch (e) {
                    this.reportExceptionInDerivation(e)
                }
            }
            endBatch()
        }
    }

this.onInvalidate Here we start processing cb. The core logic is:

 function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
    // ...
    // 把 Reaction 和之前收集的被观察者状态都置为 UP_TO_DATE
    changeDependenciesStateTo0(derivation)
    derivation.newObserving = new Array(derivation.observing.length + 100)
    // 记录新的依赖的数量
    derivation.unboundDepsCount = 0
    // 每次执行都分配一个 uid
    derivation.runId = ++globalState.runId
    // 当前 Derivation 记录到全局的 trackingDerivation 中,这样被观察的 Observable 在其 reportObserved 方法中就能获取到该 Derivation
    const prevTracking = globalState.trackingDerivation
    globalState.trackingDerivation = derivation
    let result
    if (globalState.disableErrorBoundaries === true) {
        // debug 环境不 catch 异常,若出错堆栈清晰
        result = f.call(context)
    } else {
        try {
            // 执行响应函数 cb ,收集使用到的所有依赖,加入 newObserving 数组中
            result = f.call(context)
        } catch (e) {
            result = new CaughtException(e)
        }
    }
    globalState.trackingDerivation = prevTracking
    // 比较新旧依赖,更新依赖
    bindDependencies(derivation)
    // 如果配置了 requiresObservable 但是 cb 内没引用 OV 的话,报警告
    warnAboutDerivationWithoutDependencies(derivation)
        // ...
}
What's going on in the getter? (tracking dependencies)

When executing cb, the value of the observable is read. Taking the decorator decoration method as an example, it will go to:

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

What this.values.get(key) gets is OV, the get of OV:

 public get(): T {
        this.reportObserved()
        return this.dehanceValue(this.value)
}

function reportObserved(observable: IObservable): boolean {
    // ...
    const derivation = globalState.trackingDerivation
    if (derivation !== null) {
        // 避免重复收集 OV 
        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) {
        // 如果 OV 没有 derivation 观察了,准备清除 Observable 
        queueForUnobservation(observable)
    }

    return false
}

In fact, OV is collected on React's newObserving, and the tracking dependency is over.

handle dependencies

The next step is to process the collected dependencies:

  1. Replace Derivation's dependencies array with newly collected dependencies
  2. Find the disjoint elements of the old and new dependency arrays, unbind the relationship between the disjoint OV in the old dependency array and the Derivation (OV no longer collects Derivation), and bind the relationship between the disjoint OV in the new dependency array and the Derivation
 function bindDependencies(derivation: IDerivation) {
    // invariant(derivation.dependenciesState !== IDerivationState.NOT_TRACKING, "INTERNAL ERROR bindDependencies expects derivation.dependenciesState !== -1");
    const prevObserving = derivation.observing
    const observing = (derivation.observing = derivation.newObserving!)
    // 记录更新依赖过程中,新观察的 Derivation 的最新状态
    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
    // 遍历新的 observing 数组,使用 diffValue 这个属性来辅助 diff 过程:
    // 所有 Observable 的 diffValue 初值都是0(要么刚被创建,继承自 BaseAtom 的初值0;
    // 要么经过上次的 bindDependencies 后,置为了0)
    // 如果 diffValue 为0,保留该 Observable,并将 diffValue 置为1
    // 如果 diffValue 为1,说明是重复的依赖,无视掉
    let i0 = 0,
        l = derivation.unboundDepsCount // 新收集的 ObservableValue 数量
    for (let i = 0; i < l; i++) {
        const dep = observing[i]
        if (dep.diffValue === 0) {
            // 这次此次 Reaction 最新收集的依赖
            dep.diffValue = 1
            // i0 不等于 i,即前面有重复的 dep 被无视,依次往前移覆盖
            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 // 只保留最新一次追踪 Reaction 收集的依赖

    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
    // 遍历 prevObserving 数组,检查 diffValue:(经过上一次的 bindDependencies  后,该数组中不会有重复)
    // 如果为 0,说明没有在 newObserving 中出现,调用 removeObserver 将 dep 和 derivation 间的联系移除
    // 如果为 1,依然被观察,将 diffValue 置为0(在下面的循环有用处)
    l = prevObserving.length
    while (l--) {
        const dep = prevObserving[l]
        if (dep.diffValue === 0) {
            removeObserver(dep, derivation)
        }
        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
    // 再次遍历新的 observing 数组,检查 diffValue
    // 如果为0,说明是在上面的循环中置为了0,即是本来就被观察的依赖,什么都不做
    // 如果为1,说明是新增的依赖,调用 addObserver 新增依赖,并将 diffValue 置为0,为下一次 bindDependencies 做准备
    while (i0--) {
        const dep = observing[i0]
        if (dep.diffValue === 1) {
            dep.diffValue = 0
            addObserver(dep, derivation)
        }
    }

    // Some new observed derivations may become stale during this derivation computation
    // so they have had no chance to propagate staleness (#916)
    // 某些新观察的 Derivation 可能在依赖更新过程中过期
    // 避免这些 Derivation 没有机会传播过期的信息(#916)
    if (lowestNewObservingDerivationState !== IDerivationState.UP_TO_DATE) {
        derivation.dependenciesState = lowestNewObservingDerivationState
        derivation.onBecomeStale()
    }
}

The diffValue flag is used above to reduce the time complexity of the naive algorithm to linear. Let's give an example:

 const a = {};
const b = {};
const c = {};

const prev = [a, b];
const curr = [b, c];

// 找出不相交的 a, c 并做一些处理你会怎么做?  

// 朴素算法的处理就是 O(n^2)
prev.forEach((p, ip) => {
    curr.forEach((c, ic) => {
    // includes 时间复杂度为 O(n),假设用户用 set,has 是常数级的,暂且视此处也为常数级
    if (p !== c && prev.includes(c)) {
      // 解绑
    } 
    
    if (p !== c && !prev.includes(c)) {
      // 绑定
    } 
  })
})

If you add a diffValue as a flag, the algorithm is:

 const a = {d: 0};
const b = {d: 0};
const c = {d: 0};

const prev = [a, b];
const curr = [b, c];

curr.forEach(c => c.d = 1);
prev.forEach(p => {
    if (p.d === 0) {
            // 解绑
  }
  p.d = 0;
})
curr.forEach(c => {
  if (c.d === 1) {
    // 绑定
  }
  c.d = 0;
})

At this point, the dependencies are processed, the used OV is collected on the Derivation, and each OV also collects the derived Derivation. And set the Derivation and the state of the previously collected dependencies to UP_TO_DATE.

 derivation.dependenciesState = IDerivationState.UP_TO_DATE
OV.lowestObserverState = IDerivationState.UP_TO_DATE

The dependency status of the new binding is NOT_TRACKING | UP_TO_DATE.

What's going on in the setter?

Also take the properties decorated by decorators as an example:

 write(key: PropertyKey, newValue) {
        const instance = this.target
        // 拿到 OV
        const observable = this.values.get(key)
           // 处理计算值情况
        if (observable instanceof ComputedValue) {
            observable.set(newValue)
            return
        }
        // enhance 新值,Object.is 对比新旧值
        newValue = (observable as any).prepareNewValue(newValue)

        if (newValue !== globalState.UNCHANGED) {
              // 值变化
            // ...
            (observable as ObservableValue<any>).setNewValue(newValue)
            // ...
        }
    }

setNewValue(newValue: T) {
        const oldValue = this.value
        this.value = newValue
        this.reportChanged()
        // ...
    }

reportChanged() {
        startBatch()
              // 通知变化
        propagateChanged(this)
        endBatch()
    }

Notifying changes actually does three things:

  1. Change the state of OV to STALE
  2. Traverse all Derivations bound by OV and process
  3. After processing a Derivation, change its state to STALE
 export function propagateChanged(observable: IObservable) {
    // invariantLOS(observable, "changed start");
    if (observable.lowestObserverState === IDerivationState.STALE) return
    observable.lowestObserverState = IDerivationState.STALE

    // Ideally we use for..of here, but the downcompiled version is really slow...
    // 如果被解除 observableValue 和 Observer 的绑定关系,这里就不会遍历到。
    observable.observers.forEach(d => {
        if (d.dependenciesState === IDerivationState.UP_TO_DATE) {
            // ...
              // 遍历 OV 绑定的所有 Derivation,并处理
            d.onBecomeStale()
        }
        d.dependenciesState = IDerivationState.STALE
    })
    // invariantLOS(observable, "changed end");
}

What does d.onBecomeStale() do?

In fact, it is to add the Derivation to the schedule, schedule the execution of the Reaction, and repeat our above process.

 onBecomeStale() {
        this.schedule()
    }
Overview of the React process

Computed Value

CV is a relatively special existence, that is, as a dependency and as a derivative. It uses dependencies in its side effects, a derivation of its internal dependencies.

process

In Mobx, a class ComputedValue is also used to manage:

 class ComputedValue {
  dependenciesState = IDerivationState.NOT_TRACKING // 作为派生的初始状态
  lowestObserverState = IDerivationState.UP_TO_DATE // 作为依赖的初始状态
    observing: IObservable[] = [] // CV 作为派生,收集的所有依赖
  newObserving = null // 每 batch 执行中新收集的依赖
  observers = new Set<IDerivation>() // CV 作为依赖,收集的所有派生
  // ...
  constructor(options: IComputedValueOptions<T>) {
               // 检错机制,参数必须含 get 
        invariant(options.get, "missing option for computed: get")
            // getter 回调作为内部依赖的派生
        this.derivation = options.get!
        this.name = options.name || "ComputedValue@" + getNextId()
        // 处理 setter
            // ...
            // 对于新旧计算结果的对比方法,默认 Object.is
        this.equals =
            options.equals ||
            ((options as any).compareStructural || (options as any).struct
                ? comparer.structural
                : comparer.default)
            // getter 回调计算的上下文
        this.scope = options.context
                // 是否必须要求在副作用内使用计算属性
        this.requiresReaction = !!options.requiresReaction
            // 是否一直强制绑定计算属性以及内部依赖。 (默认当计算属性没被用时,会同步解绑计算属性与其内部依赖)
        this.keepAlive = !!options.keepAlive
    }
}

Every time a computed property is accessed, the internal get method is triggered, which does two things:

  1. notification is observed
  2. Evaluate if computation is required, and if so, handle some state changes.
 public get(): T {
        if (this.isComputing) fail(`Cycle detected in computation ${this.name}: ${this.derivation}`)
        if (globalState.inBatch === 0 && this.observers.size === 0 && !this.keepAlive) {
            // 在非副作用里访问,简单计算出返回值
            if (shouldCompute(this)) {
                this.warnAboutUntrackedRead()
                startBatch() // See perf test 'computed memoization'
                this.value = this.computeValue(false)
                endBatch()
            }
        } else {
              // 在副作用里访问
            // 通知被观察,加入 Reaction.newObserving,之后会建立起计算属性与其派生的绑定关系
            reportObserved(this)
            // 评估作为 Derivation 是否需要计算
            // 若需要,重新计算完后,自身作为 D 的状态变为 U_T_D 。依赖状态变更为 U_T_D
            // 若值有改变,则改变自身作为 OV 的状态为 STALE,收集的观察者(第一次读取时没有)的状为 STALE 
            if (shouldCompute(this)) if (this.trackAndCompute()) propagateChangeConfirmed(this)
        }
        const result = this.value!

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

The first visit definitely needs to be calculated, let's take a look at the method of evaluating the calculation:

 export function shouldCompute(derivation: IDerivation): boolean {
    switch (derivation.dependenciesState) {
        case IDerivationState.UP_TO_DATE:
            return false
        case IDerivationState.NOT_TRACKING: // 第一次访问时
        case IDerivationState.STALE:
            return true
        case IDerivationState.POSSIBLY_STALE: {
            // 暂时跳过
        }
    }
}

Start computing and tracking dependencies after knowing that computation is allowed

 private trackAndCompute(): boolean {
        // ...
        const oldValue = this.value
        // 有没有解除计算属性与其内部依赖的绑定关系,第一次肯定是没有绑定关系的
        const wasSuspended =
            /* see #1208 */ this.dependenciesState === IDerivationState.NOT_TRACKING
        // 新计算的值
        const newValue = this.computeValue(true)
        const changed =
            wasSuspended ||
            isCaughtException(oldValue) ||
            isCaughtException(newValue) ||
            !this.equals(oldValue, newValue)
        if (changed) {
            // 若有改变则赋新值
            this.value = newValue
        }
        return changed
    }

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
    }

trackDerivedFunction is very familiar. I analyzed it when I explained AutoRun. It mainly did three things, in fact, it calculated and tracked dependencies:

  1. Because the fork is about to execute, change the state of the fork and its dependencies to U_T_D
  2. Execute Derivation
  3. Establish a binding relationship between derivatives and dependencies

If the result changes, execute propagateChangeConfirmed(this), that is, change CV as a dependency and its derived state to STALE.

 export function propagateChangeConfirmed(observable: IObservable) {
    // invariantLOS(observable, "confirmed start");
    // 让 computedValue 作为 OV ,改变自身状态与其收集的 Derivation 都为不稳定
    if (observable.lowestObserverState === IDerivationState.STALE) return
    observable.lowestObserverState = IDerivationState.STALE
        // 第一次访问计算属性时,还未建立起派生与计算属性的绑定关系,所以 observers 为空
      // 之后访问的情况下,就会把派生的状态由 P_S 转为 STALE 了
    observable.observers.forEach(d => {
        if (d.dependenciesState === IDerivationState.POSSIBLY_STALE)
            d.dependenciesState = IDerivationState.STALE
        else if (
            d.dependenciesState === IDerivationState.UP_TO_DATE // this happens during computing of `d`, just keep lowestObserverState up to date.
        ) // 当派生已经开始重新处理时会遇到这个情况,此时不需要改变计算属性作为 OV 的状态和派生的状态了,因为派生已经重新处理了,并且也会拿到最新的计算值,此时直接把计算属性作为 OV 的状态设为 U_T_D 就好
          // 比如,计算属性的派生是与依赖 A 与计算属性绑定的
          // 某个 action 里面先改变了计算属性的深依赖值,再改变依赖 A 的值
                    // 此时派生的状态会先变 P_S ,再变为 STALE,
          // 在一轮 batch 结束后,重新处理派生 Reaction,会直接重新计算计算属性的值,走到这个判断条件内,不需要再管派生应不应该重新处理了,人家已经由依赖 A 的变化确定要处理了。
            observable.lowestObserverState = IDerivationState.UP_TO_DATE
    })
    // invariantLOS(observable, "confirmed end");
}

After the derivation of the computed property is processed, the computed property will be used as the state of the dependency and the state of the derivation itself will be changed to U_T_D, waiting for the next dependency change to be processed again.

Other dependency changes within the derivation of the computed property

In this case, the value of the computed property will be read again, but since shouldCompute will evaluate the derived state of the computed property as U_T_D, that is, its deep dependency has not changed, it will directly use the result of the previous calculation, and there will be no other any processing.

Dependency changes for computed properties

In this case, it will follow the normal process of dependency change. As mentioned in AutoRun, trigger the deeply dependent setter, change the state of the deep dependency and derived (computed property) to STALE, and then execute the derived onBecomeStale() method.

The onBecomeStale() method is for React to join the schedule and wait for the end of the batch to process the React again. A little change for computed properties:

  1. Will change the state of the computed property as OV to P_S, and change the state of the derivative of the computed property to P_S
  2. Schedule the derivation of computed properties
 onBecomeStale() {
        propagateMaybeChanged(this)
    }

export function propagateMaybeChanged(observable: IObservable) {
    // invariantLOS(observable, "maybe start");
    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)
            }
              // 将 Reaction 加入计划表,等待重新处理
            d.onBecomeStale()
        }
    })
    // invariantLOS(observable, "maybe end");
}

Then, when the deeply dependent batch ends, the Reaction will be taken out from the schedule for processing, and returned to the logic in Autorun:

 runReaction() {
        if (!this.isDisposed) {
            // 开启一个事务处理,因为运行 cb 的过程中可能会再加 Reaction 到计划表(比如依赖更新)
            startBatch()
            this._isScheduled = false
            // 判断 Reaction 收集的依赖状态
            // 如状态机所示,只有在 NO_TRACKING | STALE | 判断 COMPUTED 值变化时才会执行 Reaction 
            if (shouldCompute(this)) {
                this._isTrackPending = true

                try {
                      // 处理 cb 
                    this.onInvalidate()
                    // ...
                } catch (e) {
                    this.reportExceptionInDerivation(e)
                }
            }
            endBatch()
        }
    }
Evaluation calculation

At this time, the derived state is P_S, and the following logic will be reached when evaluating the calculation:

  1. Find the computed property of the derived dependency, and recalculate it, changing the computed property's deep dependency and itself as a derived state to U_T_D
  2. If the recalculated value changes, the state of the computed property as OV and the derived state of the computed property will be changed to STALE, then the derivation is processed, the derivation callback is re-executed, and the dependency binding relationship is re-established.
  3. If the recalculated value does not change, the old value is returned directly, and the state of the derived and calculated attributes as OV is changed to U_T_D, preventing the derived from continuing to process.
 export function shouldCompute(derivation: IDerivation): boolean {
    switch (derivation.dependenciesState) {
        case IDerivationState.UP_TO_DATE:
            return false
        case IDerivationState.NOT_TRACKING:
        case IDerivationState.STALE:
            return true
        case IDerivationState.POSSIBLY_STALE: {
            // state propagation can occur outside of action/reactive context #2195
            const prevAllowStateReads = allowStateReadsStart(true)
            // 此处对 CV 的 get 不需要 reportObserved (untrackedStart 的作用),之后会再执行进行收集
            // 这里的主要目的是:判断重新计算的值有没有改变,然后根据结果做一些状态变更
            const prevUntracked = untrackedStart() // no need for those computeds to be reported, they will be picked up in trackDerivedFunction.
            const obs = derivation.observing, // 拿到所有 OV
                l = obs.length
            for (let i = 0; i < l; i++) {
                const obj = obs[i]
                // 找到 CV 的 OV
                if (isComputedValue(obj)) {
                    if (globalState.disableErrorBoundaries) {
                        obj.get()
                    } else {
                        try {
                              // 再次调用 get 重新计算,具体逻辑上面分析过
                            obj.get()
                        } catch (e) {
                            // we are not interested in the value *or* exception at this moment, but if there is one, notify all
                            // 如果 CV getter 执行异常,那就默认让副作用继续执行一次
                            untrackedEnd(prevUntracked)
                            allowStateReadsEnd(prevAllowStateReads)
                            return true
                        }
                    }
                    // if ComputedValue `obj` actually changed it will be computed and propagated to its observers.
                    // and `derivation` is an observer of `obj`
                    // invariantShouldCompute(derivation)
                      // 若重新计算有变化了,其派生的状态会变成 STALE
                    if ((derivation.dependenciesState as any) === IDerivationState.STALE) {
                        untrackedEnd(prevUntracked)
                        allowStateReadsEnd(prevAllowStateReads)
                        // 允许派生运行
                        return true
                    }
                }
            }
              // 如果重新计算值没有变化,则重置派生与计算属性作为依赖的状态为 U_T_D
            changeDependenciesStateTo0(derivation)
            untrackedEnd(prevUntracked)
            allowStateReadsEnd(prevAllowStateReads)
            // 不允许派生继续运行
            return false
        }
    }
}

The above achieves the purpose of directly applying the old calculated value (avoiding redundant calculations) when the computed property dependency does not change, and reprocessing the side effects (avoiding invalid side effects) only when the computed property dependency changes and the recomputed value changes .

React render

class component

We can decorate a component with @observer:

 import { observer } from 'mobx-react';

@observer
class AComponent {
  render() {
    return ...;
  };
}

The core work of the actual observer is to use the render function as a derivation (wrapped with a derivation), and then each time the track re-executes the behavior of the render to collect the dependencies. When the dependencies change, it will trigger React.Component.prototype.forceUpdate() to force Re-execute render to collect dependencies and update views.

After the NB observer is decorated, some of React's lifecycle hooks cannot be triggered, so in fact, some fake hook operations such as shouldUpdate, willUnMount and some optimization and bug fixing operations are also performed internally. For these operations, skip here and only talk about the core principle code.

Let's take a look at how the source code is implemented:

 export function observer<T extends IReactComponent>(component: T): T {
    // ... 错误操作的报警

    // ... 处理 ForwardRef 
  
    // 处理 Function component  暂且跳过
    if (
        typeof component === "function" &&
        (!component.prototype || !component.prototype.render) &&
        !component["isReactClass"] &&
        !Object.prototype.isPrototypeOf.call(React.Component, component)
    ) {
        return observerLite(component as React.StatelessComponent<any>) as T
    }
        // Class Component
    return makeClassComponentObserver(component as React.ComponentClass<any, any>) as T
}

export function makeClassComponentObserver(
    componentClass: React.ComponentClass<any, any>
): React.ComponentClass<any, any> {
      // 组件原型
    const target = componentClass.prototype

    if (componentClass[mobxObserverProperty]) {
          // 错误操作报警
        const displayName = getDisplayName(target)
        console.warn(
            `The provided component class (${displayName}) 
                has already been declared as an observer component.`
        )
    } else {
          // 表示组件已被 Mobx 作为观察者
        componentClass[mobxObserverProperty] = true
    }
        // 错误报警
    if (target.componentWillReact)
        throw new Error("The componentWillReact life-cycle event is no longer supported")
        // 实现 shouldComponentUpdate
    if (componentClass["__proto__"] !== PureComponent) {
        if (!target.shouldComponentUpdate) target.shouldComponentUpdate = observerSCU
        else if (target.shouldComponentUpdate !== observerSCU)
            // n.b. unequal check, instead of existence check, as @observer might be on superclass as well
            throw new Error(
                "It is not allowed to use shouldComponentUpdate in observer based components."
            )
    }

    // 将 Props、State 包装成 OV
    makeObservableProp(target, "props")
    makeObservableProp(target, "state")
        // 原始 render
    const baseRender = target.render
    // 被拦截的 render,只首次 mount 会调这个
    target.render = function () {
          // 原始 render 外部包了一层派生
        return makeComponentReactive.call(this, baseRender)
    }
    patch(target, "componentWillUnmount", function () {
        if (isUsingStaticRendering() === true) return
          // 组件卸载时,解绑派生与依赖的绑定,避免内存泄漏
        this.render[mobxAdminProperty]?.dispose()
        this[mobxIsUnmounted] = true

        if (!this.render[mobxAdminProperty]) {
            // Render may have been hot-swapped and/or overriden by a subclass.
            const displayName = getDisplayName(this)
            console.warn(
                `The reactive render of an observer class component (${displayName}) 
                was overriden after MobX attached. This may result in a memory leak if the 
                overriden reactive render was not properly disposed.`
            )
        }
    })
    return componentClass
}

function makeComponentReactive(render: any) {
    if (isUsingStaticRendering() === true) return render.call(this)

      // 处理 forceUpdate 带来的副作用 ...

    const initialName = getDisplayName(this)
    const baseRender = render.bind(this)

    let isRenderingPending = false

    // 创建一个派生, 带来的副作用就是第二个回调参数
    const reaction = new Reaction(`${initialName}.render()`, () => {
        if (!isRenderingPending) {
            // N.B. Getting here *before mounting* means that a component constructor has side effects (see the relevant test in misc.js)
            // This unidiomatic React usage but React will correctly warn about this so we continue as usual
            // See #85 / Pull #44
            isRenderingPending = true
            if (this[mobxIsUnmounted] !== true) {
                let hasError = true
                try {
                    // 处理 forceUpdate 带来的副作用 ...
                      // forceUpdate 强制重渲染
                    if (!this[skipRenderKey]) Component.prototype.forceUpdate.call(this)
                    hasError = false
                } finally {
                    // 处理 forceUpdate 带来的副作用 ...
                    if (hasError) reaction.dispose()
                }
            }
        }
    })

    reaction["reactComponent"] = this
    reactiveRender[mobxAdminProperty] = reaction
      // 之后 forceUpdate 的时候,重新执行的 render 都只是 reactiveRender
    this.render = reactiveRender

    function reactiveRender() {
        isRenderingPending = false
        let exception = undefined
        let rendering = undefined
        // 为该 reaction 派生收集原 render 函数内的依赖
        reaction.track(() => {
            try {
                  // 执行原 render 函数,拿到虚拟节点
                rendering = _allowStateChanges(false, baseRender)
            } catch (e) {
                exception = e
            }
        })
        if (exception) {
            throw exception
        }
          // 返回虚拟节点给 React
        return rendering
    }

    return reactiveRender.call(this)
}
functional component

The purpose of the function component is the same as that of the class component. The rerender re-collects the dependencies, and the dependency changes trigger the rerender. However, the function component cannot use the forceUpdate API, so Mobx uses a small trick of React hooks to achieve the effect of forceUpdate.

Since this small trick has more side effects, the processing in this part of mobx-react-light is very redundant, and the code is extracted to explain:

 function observerLite(baseFuncComponent) {
  return function(props) {
    const [tick, setTick] = useState(0);
    // 利用 useState 伪造 forceUpdate
    function forceUpdate() {
      setTick(tick + 1);
    }
    // 造一个函数组件的派生,useMemo 保证派生不会 rebuild
    // 组件内依赖变化时,invoke forceUpdate,rerender
    const r = useMemo(() => new Reaction('包裹函数组件的派生', forceUpdate), []);
    // 组件卸载时解绑派生与依赖,避免内存泄漏
    useEffect(() => () => r.dispose(), []);
    
    let vnodes = null;
    // 每轮 rerender 重新执行函数组件,追踪依赖
    r.track(() => {
      vnodes = baseFuncComponent({...props, tick});
    })
        // 返回虚拟节点给 React
    return vnodes;
  }
}

Rest of API

action

Taking the decorator action to decorate the function fn as an example, it actually rewrites the descriptor of fn and wraps the function body by createAction :

 return {
  // name 函数名、descriptor.value 函数体
  value: createAction(name, descriptor.value),
  enumerable: false,
  configurable: true, // See #1477
  writable: true // for typescript, this must be writable, otherwise it cannot inherit :/ (see inheritable actions test)
}
 export function createAction(actionName: string, fn: Function, ref?: Object): Function & IAction {
    // ...
      // 外部调用 fn 时,真正执行的是这个方法
    const res = function() { 
          // executeAction 内部核心工作就是让 fn 的执行处于一轮事务当中
        return executeAction(actionName, fn, ref || this, arguments)
    }
    ;(res as any).isMobxAction = true
    // ...
    return res as any
}
 export function executeAction(actionName: string, fn: Function, scope?: any, args?: IArguments) {
    const runInfo = _startAction(actionName, scope, args)
    try {
        return fn.apply(scope, args)
    } catch (err) {
        runInfo.error = err
        throw err
    } finally {
        _endAction(runInfo)
    }
}

// startAction 除了 startBatch 以外的操作,都是为了确实新开启一轮事务的纯净性,不被之前上下文的操作所影响。
export function _startAction(actionName: string, scope: any, args?: IArguments): IActionRunInfo {
    // ...
    let startTime: number = 0   
      // 在 action 里,对 OV 的读取不收集方法 fn。因为 action 方法并不是副作用,而是要改变依赖的动作。
    const prevDerivation = untrackedStart()
    startBatch() // 开启一轮新事务
      // 允许对依赖写
    const prevAllowStateChanges = allowStateChangesStart(true)
    // 允许对依赖读
    const prevAllowStateReads = allowStateReadsStart(true)
    // 记录该轮事务的一些信息,方便 endAction 时回退,保持开启事务前的状态纯净。
    const runInfo = {
        prevDerivation,
        prevAllowStateChanges,
        prevAllowStateReads,
        notifySpy,
        startTime,
        actionId: nextActionId++,
        parentActionId: currentActionId
    }
    currentActionId = runInfo.actionId
    return runInfo
}

// 除了结束事务的操作,其余都是根据记录的该轮事务的一些信息,回退保持开启事务前的状态纯净。
export function _endAction(runInfo: IActionRunInfo) {
    if (currentActionId !== runInfo.actionId) {
        fail("invalid action stack. did you forget to finish an action?")
    }
    currentActionId = runInfo.parentActionId

    if (runInfo.error !== undefined) {
        globalState.suppressReactionErrors = true
    }
      // 回退开启事务前依赖的改变权限
    allowStateChangesEnd(runInfo.prevAllowStateChanges)
    // 回退开启事务前依赖的读取权限
    allowStateReadsEnd(runInfo.prevAllowStateReads)
    // 结束事务,准备批量处理收集的 Reaction
    endBatch()
      // 回退开启事务前的派生追踪
    untrackedEnd(runInfo.prevDerivation)
    // ...
    globalState.suppressReactionErrors = false
}

In fact, the source code is clear at a glance. The main purpose is to make the function execution of the action in a new round of transactions. The advantage is that when the dependency of a Derivation is changed many times, it is only processed once. Return to the concept of transaction mentioned above:

An Action starts and ends with the start and end of a transaction, ensuring that changes to the state in the Action (possibly multiple times) only trigger a re-execution of the React.

Additional API

Additional APIs should be relatively easy to read after familiarizing yourself with all the above. Since there are too many APIs, I will not analyze them one by one, and I am interested in digging by myself.

Mobx Design Thinking

Mobx author Michel Weststrate has explained the Mobx design concept in a tweet , but it is a bit too detailed, and students who are not familiar with the Mobx mechanism may not understand it. Below, based on this tweet and the above source code, I will extract it in Chinese. If you are interested, you can go to the original text.

It is always better to react to state changes than to act on state changes

In response to this, it is actually the same as the concept of Vue responsive or Redux delivery, which is data-driven .

To analyze this sentence again, "reacting" means that the binding relationship between state and side effects is done by the framework (library) for you, and state changes are automatically notified to side effects without the user (developer) handling it manually.

"Take action" is to manually notify the side effect update when the user is aware of the state change. There is at least one operation that the user must do: manually subscribe to state changes in side effects, which brings at least two drawbacks:

  1. The redundancy of subscriptions cannot be guaranteed. There may be too many subscriptions or too few subscriptions, resulting in applications that do not meet expectations.
  2. It will make the business code more dirty and difficult to organize

Minimal, consistent set of subscriptions

Take render as an example of a side effect, if there is a conditional statement in render:

 render() {
  if (依赖 A) {
    return 组件 1;
  }
  return 依赖 B ? 组件 2 : 组件 3;
}

First of all, if it is handed over to the user to subscribe manually, it must only rely on the status of A and B to subscribe together. If there is less subscription, the expected re-render cannot occur.

How about handing it over to the framework to handle it? Of course, there is nothing wrong with relying on A and B to subscribe together, but assuming that both A and B have values when they are initialized, do we need to make the render subscription depend on the state of B?

No need, why? Think about the effect of re-render if the state of dependency B changes at this time?

Therefore, it is redundant to subscribe to all the states at the time of initialization. If the application is complex and has many states, there will be more unnecessary memory allocations, which will lead to loss of performance.

Therefore, Mobx implements a mechanism for handling dependencies at runtime , ensuring that side effects are bound to the smallest and consistent subscription set. See the "What's in the getter?" and "Handling dependencies" sections above for the source code.

Reasonableness of Derived Computation

Speaking of human words is: to eliminate lost computing, redundant computing .

Loss of computation: Mobx's strategy is to introduce the concept of a state machine to manage dependencies and derivations, so that the logic of mathematics ensures that computation will not be lost.

Redundant calculation:

  1. For non-computed attribute states, the concept of transaction is introduced to ensure that all synchronous changes to the state in the same batch, the derivation corresponding to the state is calculated only once.
  2. For a computed property, when the computed property is used as a derivation, when its dependencies change, the computed property will not be recalculated immediately, but will not be recalculated until the computed property itself is used as the derivation bound to the state to use the computed property value again. And computing the same value prevents the derivation from continuing to process.

Versatility (added by the author)

As far as the Mobx library itself is concerned, there is no binding relationship with UI render, and no binding relationship with the asynchronous mechanism in event loop.

So Mobx is not like Vue 2.x reactive processing, it needs to collect Wachter and then iteratively process the side effects corresponding to Wachter asynchronously before ui render. The update granularity is also differe


Sadhu
395 声望9 粉丝

rookie rookie rookie