mobx源码解读-computed及状态调整策略

726 阅读7分钟
var student = mobx.observable({
  language: 100,
  mathematics: 90,
  name:'张三'
});

var total = mobx.computed(() => {
  return student.language + student.mathematics;
});

mobx.autorun(() => {
  console.log(student.name + '的总分:' + total);
});

student.mathematics = 100

1.computed函数

export const computed: IComputed = function computed(arg1, arg2, arg3) {
    if (typeof arg2 === "string") {
        // @computed
        return computedDecorator.apply(null, arguments)
    }
    if (arg1 !== null && typeof arg1 === "object" && arguments.length === 1) {
        // @computed({ options })
        return computedDecorator.apply(null, arguments)
    }

    // arg2为object或function,
    // 为object则为IComputedValueOptions
    // 为function则为setter
    const opts: IComputedValueOptions<any> = typeof arg2 === "object" ? arg2 : {}
    opts.get = arg1
    opts.set = typeof arg2 === "function" ? arg2 : opts.set
    opts.name = opts.name || arg1.name || "" /* for generated name */

    return new ComputedValue(opts)
} as any

computed.struct = computedStructDecorator

先跳过前边两个if(装饰器部分),直接看computed(...),通过IComputed来看,computed(...)的参数为两种情况。

  • (func: () => T, setter: (v: T) => void)
  • (func: () => T, options?: IComputedValueOptions)
interface IComputed {
    <T>(options: IComputedValueOptions<T>): any // decorator
    <T>(func: () => T, setter: (v: T) => void): IComputedValue<T> // normal usage
    <T>(func: () => T, options?: IComputedValueOptions<T>): IComputedValue<T> // normal usage
    (target: Object, key: string | symbol, baseDescriptor?: PropertyDescriptor): void // decorator
    struct: (target: Object, key: string | symbol, baseDescriptor?: PropertyDescriptor) => void // decorator
}

computde方法返回了一个 ComputedValue类型的实例。

class ComputedValue<T> implements IObservable, IComputedValue<T>, IDerivation 

从上边的代码可以发现,ComputedValue实现了IObservable和IDerivation。这两个类型应该并不陌生,我们之前梳理autorun和observable的时候也看到过。

Reaction也实现IDerivation, IAtom继承自IObservable。这应该能看出来ComputedValue即实现了Reaction (响应)的功能也实现了Observable(被观察者)的功能。

实际上ComputedValue是一个中间层,当Reaction调用它的时候,ComputedValue就像一个Observable一样调用它的get方法处理一些事务,例如调用reportObserved。

当ComputedValue调用一个Observable时候,又触发它下一级的个Observable调用get方法。

当然ComputedValue也走自己独特之处。

1.1 计算值的get方法

public get(): T {
    // globalState.inBatch === 0 轻量级计算
    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 {
        // globalState.inBatch > 0 重量级计算
        reportObserved(this) 
        if (shouldCompute(this)) if (this.trackAndCompute())
            propagateChangeConfirmed(this)
    }
    const result = this.value!
    return result
}

get方法根据globalState.inBatch === 0 划分了两种计算方式

  • 1)globalState.inBatch === 0 调用computeValue
  • 2)globalState.inBatch > 0 调用trackAndCompute

我们先将他们分别命名为1)轻量级计算、2)重量级计算;接下来我会说明为什么叫做轻量级和重量级。

1.1.1 computeValue方法通过执行回调函数进行计算

computeValue(track: boolean) {
    let res: T | CaughtException
    if (track) {
        res = trackDerivedFunction(this, this.derivation, this.scope)
    } else {
        res = this.derivation.call(this.scope)
    }
    return res
}

computeValue根据参数track又划分了两种情况

  • 1)track为true,调用trackDerivedFunction(trackDerivedFunction之前也说到过,除了执行回调还会更新依赖关系、处理状态等)计算
  • 2)track为false,调用this.derivation(computed的回调)计算

轻量级计算中computeValue(false)参数为false所以,直接调用this.derivation方法计算,所以称这种计算方式为轻量级计算。

1.1.2 trackAndCompute

重量级计算种调用了computeValue(true),此时参数为true。采用trackDerivedFunction的计算方式,先执行回调再更新依赖关系。

private trackAndCompute(): boolean {
    ...
    const newValue = this.computeValue(true)

    const changed =
        wasSuspended ||
        isCaughtException(oldValue) ||
        isCaughtException(newValue) ||
        !this.equals(oldValue, newValue)

    if (changed) {
        this.value = newValue
    }

    return changed
}

2 mobx的状态调整策略。

说完了计算方式,我们再来看一下这几个方法 propagateChanged,propagateChangeConfirmed,propagateMaybeChanged,shouldCompute

export function propagateChanged(observable: IObservable) {
    if (observable.lowestObserverState === IDerivationState.STALE) return
    observable.lowestObserverState = IDerivationState.STALE
    observable.observers.forEach(d => {
        if (d.dependenciesState === IDerivationState.UP_TO_DATE) {
            d.onBecomeStale()
        }
        d.dependenciesState = IDerivationState.STALE
    })
}
---------------------------------------------------------
export function propagateChangeConfirmed(observable: IObservable) {
    if (observable.lowestObserverState === IDerivationState.STALE) return
    observable.lowestObserverState = IDerivationState.STALE
    observable.observers.forEach(d => {
        if (d.dependenciesState === IDerivationState.POSSIBLY_STALE)
            d.dependenciesState = IDerivationState.STALE
        else if (
            d.dependenciesState === IDerivationState.UP_TO_DATE 
        )
            observable.lowestObserverState = IDerivationState.UP_TO_DATE
    })
}
---------------------------------------------------------
export function propagateMaybeChanged(observable: IObservable) {
    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
            d.onBecomeStale()
        }
    })
}
---------------------------------------------------------
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: {
            const prev = globalState.trackingDerivation
            globalState.trackingDerivation = null
            const obs = derivation.observing,
                l = obs.length
            for (let i = 0; i < l; i++) {
                const obj = obs[i]
                if (isComputedValue(obj)) {
                    obj.get()
                    if ((derivation.dependenciesState as any) === IDerivationState.STALE) {
                        globalState.trackingDerivation = pre
                        return true
                    }
                }
            }
            changeDependenciesStateTo0(derivation)
            globalState.trackingDerivation = pre
            return false
        }
    }
}

先粗略的观察一下上边的这几个函数,我们可以看到两个常用的状态属性dependenciesState(接下来简称D属性),lowestObserverState(接下来简称L属性); mobx就是利用这两个属性控制是否执行计算的。

我们重点梳理一下dependenciesState,lowestObserverState;

  • 1)dependenciesState来源于Reaction类的,是响应者的状态属性;
  • 2)lowestObserverState来源于IObservable类,是被观察者的状态属性;
  • 3)ComputedValue同时拥有dependenciesState、lowestObserverState两个属性,因为它有时候充当被观察者,有时候充当响应。

这两种状态值取自同一个枚举值IDerivationState

export enum IDerivationState {
    NOT_TRACKING = -1, // 未跟踪的
    UP_TO_DATE = 0, // 最新的
    POSSIBLY_STALE = 1, // 可能是不新鲜的
    STALE = 2 // 不新鲜的
}

他们的代办的作用和字面上意思一致

  • 1)UP_TO_DATE为最新状态,处在这个状态时候不需要进行更新操作
  • 2)NOT_TRACKING为未跟踪的状态值,处在这个状态需要更新为另外三种状态
  • 3)STALE不新鲜,需要更新
  • 4)POSSIBLY_STALE可能不新鲜,先要查看他的子级(obsering集合)确认是否需要更新。

再回过头来分析上边提到的四个函数propagateChanged,propagateChangeConfirmed,propagateMaybeChanged,shouldCompute。

2.1 修改被观察值,修改L和D属性

第一个函数propagateChanged应该不陌生,在梳理被观察者observable时,执行更新(set)操作会调用此函数。在此函数种会对L和D属性进行修改。我们以头部的案例为代表进行分析。

var student = mobx.observable({
  language: 100,
  mathematics: 90,
  name:'张三'
});

var total = mobx.computed(() => {
  return student.language + student.mathematics;
});

mobx.autorun(() => {
  console.log(student.name + '的总分:' + total);
});

student.mathematics = 100

当修改student.mathematics值为100的时候,就会调用student.mathematics.set(), 再调用student.mathematics.reportChanged(),最后调用了propagateChanged函数。

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

export function propagateChanged(observable: IObservable) {
    if (observable.lowestObserverState === IDerivationState.STALE) return
    observable.lowestObserverState = IDerivationState.STALE
    observable.observers.forEach(d => {
        if (d.dependenciesState === IDerivationState.UP_TO_DATE) {
            d.onBecomeStale()
        }
        d.dependenciesState = IDerivationState.STALE
    })
}
  • 1)此时会将被观察者student.mathematics的L属性置为STALE(2)
  • 2)student.mathematics.observers集合中,响应对象的D属性也为STALE(student.mathematics.observers存储的是依赖它的计算值total,也就是将total的D属性置为2)
  • 3)此函数中,在修改total.D之前会先调用total.onBecomeStale方法。

2.2 通知被观察值的上一级,观察值发生了变动

propagateChanged函数,通过调用total.onBecomeStale方法告诉计算值,你依赖的值有变动。计算值调用propagateMaybeChanged函数,来更新自身及上一级的状态

onBecomeStale() {
    propagateMaybeChanged(this)
}
export function propagateMaybeChanged(observable: IObservable) {
    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
            d.onBecomeStale()
        }
    })
}

propagateMaybeChanged修改了totalL属性和上一级D属性。

  • 1)将total的L属性只为UP_TO_DATE(1),意思是他的下级可能被修改了。
  • 2)修改依赖它响应的D属性(reaction.D)也修改为UP_TO_DATE
    1. 通过调用reaction.onBecomeStale方法通知reaction子级有变动。

reaction.onBecomeStale在讲autorun的时候也提到过。我们列出一些主要步骤

  • 1)调用reaction.schedule()
  • 2)将reaction存入globalState.pendingReactions队列中;
  • 2)调用runReactions函数。
schedule() {
    if (!this._isScheduled) {
        this._isScheduled = true
        globalState.pendingReactions.push(this)
        runReactions()
    }
}

----------------------------------------------

export function runReactions() {
    if (globalState.inBatch > 0 || globalState.isRunningReactions) return
    reactionScheduler(runReactionsHelper)
}

但是在执行student.mathematics.set()时,已经开启一层任务即执行startBatch函数(globalState.inBatch++);

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

导致此时调用runReactions函数时globalState.inBatch > 0,会直接retrun。也就是此时正在执行任务种,不会进行更新操作,但是已经将等待执行的操作存到了globalState.pendingReactions队列种。

我们先来整理一下此刻相关变量的状态值

被观察者 student.mathematics.L 为 2
计算值 total.D 为 2,total.L 为 1
响应 reaction.D 为 1

执行到这里从修改被观察值后,通过propagateChanged函数层层上报,更改各级状态的 工作基本就完成了。接着执行endBatch函数结束这一层任务。

然而我们发现student.mathematics值的修改好像除了修改状态什么都没有做,并没有使total重新计算,也没有使autorun再次执行。

不着急,我们再来看一下一直被忽视的endBatch函数。

2.2 endBatch函数,重新执行Reaction的部署操作

export function endBatch() {
    if (--globalState.inBatch === 0) {
        runReactions()
        ....
    }
}

又出现了runReactions函数,是不是豁然开朗了。也就是说在endBatch中会使globalState.inBatch-1。并且如果globalState.inBatch===0,也就是没有正在执行的事务了,会再次调用runReactions函数。

export function runReactions() {
    if (globalState.inBatch > 0 || globalState.isRunningReactions) return
    reactionScheduler(runReactionsHelper)
}

function runReactionsHelper() {
    globalState.isRunningReactions = true
    const allReactions = globalState.pendingReactions
    while (allReactions.length > 0) {
        let remainingReactions = allReactions.splice(0)
        for (let i = 0, l = remainingReactions.length; i < l; i++)
            remainingReactions[i].runReaction()
    }
    globalState.isRunningReactions = false
}
//------------------------------Reaction---------------------------
runReaction() {
    if (!this.isDisposed) {
        startBatch()
        this._isScheduled = false
        if (shouldCompute(this)) {
            this.onInvalidate() // 通过构造函数传入,最终调用了track方法
        }
        endBatch()
    }
}
track(fn: () => void) {
    startBatch()
    const result = trackDerivedFunction(this, fn, undefined)
    endBatch()
}

上面这一串操作应该也不陌生,这串操作种会有一些岔路口,为了理清脉络我们用A、B、C...来命名

A)runReactions函数中,先取出globalState.pendingReactions队列中等待执行的Reaction,遍历依次调用他们的runReaction方法。

B)判断shouldCompute(this)返回结果

C)调用track函数,调用trackDerivedFunction函数

2.3 shouldCompute函数判断是否执行计算。

先来分析B的判断函数shouldCompute

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: {
            const prev = globalState.trackingDerivation
            globalState.trackingDerivation = null
            const obs = derivation.observing,
                l = obs.length
            for (let i = 0; i < l; i++) {
                const obj = obs[i]
                if (isComputedValue(obj)) {
                    obj.get()
                    if (derivation.dependenciesState === IDerivationState.STALE) {
                        globalState.trackingDerivation = pre
                        return true
                    }
                }
            }
            changeDependenciesStateTo0(derivation)
            globalState.trackingDerivation = pre
            return false
        }
    }
}

shouldCompute根据传入值的D属性,分了3中策略进行处理

  • 1)值为UP_TO_DATE(0)返回false即不进行计算
  • 2)值为NOT_TRACKING(-1)或STALE(2)返回true,需要进行计算。
  • 3)值为POSSIBLY_STALE(1)分为两种,根据reaction.observing集合中是否有计算值划分两种情况
  • 3.1)无计算值返回false
  • 3.2)有计算值调用计算值的get方法后,再判断reaction的D属性为STALE(2)返回true否则false

此时reaction的D属性也正好是POSSIBLY_STALE(1),所以会调用total.get方法。

计算值的get方法,开头我们讲了,再重新看一下

public get(): T {
    // globalState.inBatch === 0 轻量级计算
    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 {
        // globalState.inBatch > 0 重量级计算
        reportObserved(this) 
        if (shouldCompute(this)) if (this.trackAndCompute())
            propagateChangeConfirmed(this)
    }
    const result = this.value!
    return result
}

get中分为轻量和重量两种计算方式,此时会执行重量级计算

  • B-1)执行reportObserved()函数,此时因为在shouldCompute中将globalState.trackingDerivation置为null,所以不会有操作。
  • B-2)有来了一个shouldCompute函数,这次会根据total的D属性判断,此时total.D为2故返回true。
  • B-3)trackAndCompute函数,trackAndCompute会调用computeValue,computeValue调用trackDerivedFunction函数,所以直接来看trackDerivedFunction函数。
export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
    changeDependenciesStateTo0(derivation)
    ...
    let result = f.call(context)
    ...
    bindDependencies(derivation)
    return result
}
  • B-3-1)changeDependenciesStateTo0函数将total.D属性重新设置为0,再将存在observing属性中的依赖值L属性也变成0,也就是student.mathematics.L重新置为0。

此时变量的状态值

被观察者 student.mathematics.L 为 0
计算值 total.D 为 0,total.L 为 1
响应 reaction.D 为 1
  • B-3-2) f.call(context)重新执行了计算。
  • B-4)执行propagateChangeConfirmed函数,此时total.L为1所以会执行下边的操作修改状态
export function propagateChangeConfirmed(observable: IObservable) {
    if (observable.lowestObserverState === IDerivationState.STALE) return
    observable.lowestObserverState = IDerivationState.STALE

    observable.observers.forEach(d => {
        if (d.dependenciesState === IDerivationState.POSSIBLY_STALE)
            d.dependenciesState = IDerivationState.STALE
        else if (
            d.dependenciesState === IDerivationState.UP_TO_DATE
        )
            observable.lowestObserverState = IDerivationState.UP_TO_DATE
    })

}
  • 1)修改total.L为2。
  • 2)遍历total.observers集合,即调用total的响应Reaction,目前只有reaction,即reaction.D 从 1修改为2。

此时变量的状态值

被观察者 student.mathematics.L 为 0
计算值 total.D 为 0,total.L 为 2
响应 reaction.D 为 2

回到B的shouldCompute判断种,此时reaction.D 为 2所以返回true。

2.4 trackDerivedFunction函数将状态全部归零。

C操作又一次调用了trackDerivedFunction执行计算。

export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
    changeDependenciesStateTo0(derivation)
    ...
    let result = f.call(context)
    ...
    bindDependencies(derivation)
    return result
}
  • C-1)changeDependenciesStateTo0将reaction.D和,他的下一级total的L属性设置为0;
  • C-2)f.call(context)重新执行计算。
  • C-3) bindDependencies更新依赖关系。 此时变量的状态值
被观察者 student.mathematics.L 为 0
计算值 total.D 为 0,total.L 为 0
响应 reaction.D 为 0

此时状态已经全部归0,total和reaction也都重新执行了计算。一次修改到更新的操作流程已经捋清楚了。

总结

computed的文档中有这样一段描述

image.png

通过上边说的“状态调整策略”再来理解这句话,为什么说“计算中使用的数据没有更改,计算属性将不会重新运行”就很明白了。

  • 1)只有数据更改,才会修改提升自己的L属性和依赖值D属性,并层层上报修改状态层层修改状态。
  • 2)向下执行时候,会调用shouldCompute函数通过判断D属性决定否需要更新,更新过程中会逐步将状态归零。