mobx 源码学习二

collect收集依赖

本文是 [mobx 源码初步认识] 第二篇

本系列文章全部采用 mobx 较新版本:[v6.2.0]

技术前提

在阅读之前,希望你对以下技术有所了解或实践,不然可能会影响你对本文的理解

  1. ES6 装饰器:decorator
  2. ES6 代理:proxy
  3. 定义对象属性:Object.defineProperty
  4. 实现简易版 观察者模式
  5. mobx中observable-object和observable-value包装后的结果有所了解(回顾上次mbox构建object)

准备

一、目录结构

├── src
│   ├── api // 进行劫持方法和作出反应的 api
│   ├── core // 全局状态、函数等
│   ├── types // 重写关于 object,array 等类型的 api
│   ├── utils // 工具方法
│   ├── internal.ts
│   └── mobx.ts // 全局导出
└── package.json

二、通过构建后的ObservableValue 对象

1.ObservableValue

ObservableValue充当可观察对象,其中 observers 代表依赖它的观察者集合,lowestObserverState 代表可观察对象的状态(全文的 observable 代表 observableValue,observableObject,observableArray...)

export interface IObservable extends IDepTreeNode {
    diffValue_: number
    lastAccessedBy_: number
    isBeingObserved_: boolean
    lowestObserverState_: IDerivationState_ // Used to avoid redundant propagations
    isPendingUnobservation_: boolean // Used to push itself to global.pendingUnobservations at most once per batch.
    observers_: Set<IDerivation>   // 使用set去重
    onBUO(): void
    onBO(): void
    onBUOL: Set<Lambda> | undefined
    onBOL: Set<Lambda> | undefined
}
  1. Atom
export class Atom implements IAtom {
    isPendingUnobservation_ = false // for effective unobserving. BaseAtom has true, for extra optimization, so its onBecomeUnobserved never gets called, because it's not needed
    isBeingObserved_ = false
    observers_ = new Set<IDerivation>()

    diffValue_ = 0
    lastAccessedBy_ = 0
    lowestObserverState_ = IDerivationState_.NOT_TRACKING_
    constructor(public name_ = __DEV__ ? "Atom@" + getNextId() : "Atom") {}
    public onBOL: Set<Lambda> | undefined
    public onBUOL: Set<Lambda> | undefined
    public onBO() {
        if (this.onBOL) {
            this.onBOL.forEach(listener => listener())
        }
    }
    public onBUO() {
        if (this.onBUOL) {
            this.onBUOL.forEach(listener => listener())
        }
    }
    // 通过get时 收集依赖调用
    public reportObserved(): boolean {
        return reportObserved(this)
    }
    // 通过set是 触发依赖更新
    public reportChanged() {
        startBatch()
        propagateChanged(this)
        endBatch()
    }

    toString() {
        return this.name_
    }
}

三、收集依赖

  1. mobx中由Derivationreaction也是实现它)充当观察者,其中observing代表依赖的可观察对象集合,dependenciesState` 代表观察者状态
export interface IDerivation extends IDepTreeNode {
    observing_: IObservable[]             // 依赖的可观察对象集合
    newObserving_: null | IObservable[]     // 
    dependenciesState_: IDerivationState_   // 观察者状态
    runId_: number
    unboundDepsCount_: number
    onBecomeStale_(): void
    isTracing_: TraceMode
    requiresObservable_?: boolean
}

2.Reaction 对象

export class Reaction implements IDerivation, IReactionPublic {
    observing_: IObservable[] = [] 
    newObserving_: IObservable[] = []
    dependenciesState_ = IDerivationState_.NOT_TRACKING_
    diffValue_ = 0
    runId_ = 0
    unboundDepsCount_ = 0
    isDisposed_ = false
    isScheduled_ = false
    isTrackPending_ = false
    isRunning_ = false
    isTracing_: TraceMode = TraceMode.NONE

    constructor(
        public name_: string = __DEV__ ? "Reaction@" + getNextId() : "Reaction",
        private onInvalidate_: () => void,
        private errorHandler_?: (error: any, derivation: IDerivation) => void,
        public requiresObservable_ = false
    ) { }

    onBecomeStale_() {
        this.schedule_()
    }
    schedule_() {
        if (!this.isScheduled_) {
            this.isScheduled_ = true
            globalState.pendingReactions.push(this)
            runReactions()
        }
    }
    runReaction_() {
        /*xxxxx*/
    }
    // 依赖收集
    track(fn: () => void) {
        /*xxxxx*/
        const prevReaction = globalState.trackingContext 
        globalState.trackingContext = this
        const result = trackDerivedFunction(this, fn, undefined)
        globalState.trackingContext = prevReaction
    }

    reportExceptionInDerivation_(error: any) {
    }
    dispose() {
        if (!this.isDisposed_) {
            this.isDisposed_ = true
            if (!this.isRunning_) {
                startBatch()
                clearObserving(this)
                endBatch()
            }
        }
    }
    getDisposer_(): IReactionDisposer {
        const r = this.dispose.bind(this) as IReactionDisposer
        r[$mobx] = this
        return r
    }
    toString() {
        return `Reaction[${this.name_}]`
    }
    trace(enterBreakPoint: boolean = false) {
        trace(this, enterBreakPoint)
    }
}

3,trackDerivedFunction发起收集依赖和绑定依赖

先调用 changeDependenciesStateTo0 方法将 derivation 和 observing 置为稳定态 UP_TO_DATE,主要是方便后续判断是否处在收集依赖阶段(下章会讲)

trackingDerivation 为在全局正在收集依赖的 derivation,初始为 null

使用 newObserving 临时变量储存依赖和为后面的绑定依赖做准备

然后是“切分支”,见下代码,后面会多次用到

其主要目的是保存正在收集依赖的 derivation,以防 f.call(context) 时递归调用了 trackDerivedFunction,而找不到上下文

聪明的你可能猜到了其中包含 ComputedValue

f.call(context) 正式执行 autorun 中传入的函数,newObserving 数组也是在这一阶段填充的

最后是 bindDependencies 绑定依赖

export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
    const prevAllowStateReads = allowStateReadsStart(true)
    // pre allocate array allocation + room for variation in deps
    // array will be trimmed by bindDependencies
    changeDependenciesStateTo0(derivation)
    derivation.newObserving_ = new Array(derivation.observing_.length + 100)
    derivation.unboundDepsCount_ = 0
    derivation.runId_ = ++globalState.runId
    const prevTracking = globalState.trackingDerivation
    globalState.trackingDerivation = derivation
    globalState.inBatch++
    let result
    if (globalState.disableErrorBoundaries === true) {
        result = f.call(context)
    } else {
        try {
            result = f.call(context)
        } catch (e) {
            result = new CaughtException(e)
        }
    }
    globalState.inBatch--
    globalState.trackingDerivation = prevTracking
    bindDependencies(derivation)

    warnAboutDerivationWithoutDependencies(derivation)
    allowStateReadsEnd(prevAllowStateReads)
    return result
}

4,依赖收集,还记得之前说过的构建对象object后的拦截属性 get,set拦截属性方法吗?对了,我们在获取一个对象属性的时候语法:

{obj.A}   // 此方法会被 proxy中的get拦截,并且上报依赖该值的对象

我们看上报的方法

其中observable 是通过装饰器构建后的对象,包括object,array,value.map,set 等

把我们构建的对象收集到 globalState.trackingDerivation中

export 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_++] = observable
            if (!observable.isBeingObserved_ && globalState.trackingContext) {
                observable.isBeingObserved_ = true
                observable.onBO()
            }
        }
        return true
    } else if (observable.observers_.size === 0 && globalState.inBatch > 0) {
        queueForUnobservation(observable)
    }
    return false
}

5,修改更新,当然修改数据后出发更新需要在proxy拦截属性set时收集改变

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...
    observable.observers_.forEach(d => {
        if (d.dependenciesState_ === IDerivationState_.UP_TO_DATE_) {
            if (__DEV__ && d.isTracing_ !== TraceMode.NONE) {
                logTraceInfo(d, observable)
            }
            d.onBecomeStale_()
        }
        d.dependenciesState_ = IDerivationState_.STALE_
    })
    // invariantLOS(observable, "changed end");
}

四.computedValue

ComputedValue有点特殊同时实现了IDerivationIObservable接口,所以它拥有上述两点的特征。因为ComputedValue可以被Derivation依赖,同时也可以依赖ObservableValue`

export class ComputedValue<T> implements IObservable, IComputedValue<T>, IDerivation {
    dependenciesState_ = IDerivationState_.NOT_TRACKING_
    observing_: IObservable[] = [] // nodes we are looking at. Our value depends on these nodes
    newObserving_ = null // during tracking it's an array with new observed observers
    isBeingObserved_ = false
    isPendingUnobservation_: boolean = false
    observers_ = new Set<IDerivation>()
    diffValue_ = 0
    runId_ = 0
    lastAccessedBy_ = 0
    lowestObserverState_ = IDerivationState_.UP_TO_DATE_
    unboundDepsCount_ = 0
    protected value_: T | undefined | CaughtException = new CaughtException(null)
    name_: string
    triggeredBy_?: string
    isComputing_: boolean = false // to check for cycles
    isRunningSetter_: boolean = false
    derivation: () => T // N.B: unminified as it is used by MST
    setter_?: (value: T) => void
    isTracing_: TraceMode = TraceMode.NONE
    scope_: Object | undefined
    private equals_: IEqualsComparer<any>
    private requiresReaction_: boolean
    keepAlive_: boolean

    constructor(options: IComputedValueOptions<T>) {
        if (!options.get) die(31)
        this.derivation = options.get!
        this.name_ = options.name || (__DEV__ ? "ComputedValue@" + getNextId() : "ComputedValue")
        if (options.set) {
            this.setter_ = createAction(
                __DEV__ ? this.name_ + "-setter" : "ComputedValue-setter",
                options.set
            ) as any
        }
        this.equals_ =
            options.equals ||
            ((options as any).compareStructural || (options as any).struct
                ? comparer.structural
                : comparer.default)
        this.scope_ = options.context
        this.requiresReaction_ = !!options.requiresReaction
        this.keepAlive_ = !!options.keepAlive
    }

    onBecomeStale_() {
        propagateMaybeChanged(this)
    }

    public onBOL: Set<Lambda> | undefined
    public onBUOL: Set<Lambda> | undefined

    public onBO() {
        if (this.onBOL) {
            this.onBOL.forEach(listener => listener())
        }
    }

    public onBUO() {
        if (this.onBUOL) {
            this.onBUOL.forEach(listener => listener())
        }
    }

    public get(): T {
        if (this.isComputing_) die(32, this.name_, this.derivation)
        if (
            globalState.inBatch === 0 &&
            // !globalState.trackingDerivatpion &&
            this.observers_.size === 0 &&
            !this.keepAlive_
        ) {
            if (shouldCompute(this)) {
                this.warnAboutUntrackedRead_()
                startBatch() // See perf test 'computed memoization'
                this.value_ = this.computeValue_(false)
                endBatch()
            }
        } else {
            reportObserved(this)
            if (shouldCompute(this)) {
                let prevTrackingContext = globalState.trackingContext
                if (this.keepAlive_ && !prevTrackingContext) globalState.trackingContext = this
                if (this.trackAndCompute()) propagateChangeConfirmed(this)
                globalState.trackingContext = prevTrackingContext
            }
        }
        const result = this.value_!

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

    public set(value: T) {
        if (this.setter_) {
            if (this.isRunningSetter_) die(33, this.name_)
            this.isRunningSetter_ = true
            try {
                this.setter_.call(this.scope_, value)
            } finally {
                this.isRunningSetter_ = false
            }
        } else die(34, this.name_)
    }

    trackAndCompute(): boolean {
        // N.B: unminified as it is used by MST
        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
        // don't allow state changes during computation
        const prev = allowStateChangesStart(false)
        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)
                }
            }
        }
        allowStateChangesEnd(prev)
        this.isComputing_ = false
        return res
    }

    suspend_() {
        if (!this.keepAlive_) {
            clearObserving(this)
            this.value_ = undefined // don't hold on to computed value!
        }
    }

    observe_(listener: (change: IComputedDidChange<T>) => void, fireImmediately?: boolean): Lambda {
        let firstTime = true
        let prevValue: T | undefined = undefined
        return autorun(() => {
            // TODO: why is this in a different place than the spyReport() function? in all other observables it's called in the same place
            let newValue = this.get()
            if (!firstTime || fireImmediately) {
                const prevU = untrackedStart()
                listener({
                    observableKind: "computed",
                    debugObjectName: this.name_,
                    type: UPDATE,
                    object: this,
                    newValue,
                    oldValue: prevValue
                })
                untrackedEnd(prevU)
            }
            firstTime = false
            prevValue = newValue
        })
    }

    warnAboutUntrackedRead_() {
        if (!__DEV__) return
        if (this.requiresReaction_ === true) {
            die(`[mobx] Computed value ${this.name_} is read outside a reactive context`)
        }
        if (this.isTracing_ !== TraceMode.NONE) {
            console.log(
                `[mobx.trace] '${this.name_}' is being read outside a reactive context. Doing a full recompute`
            )
        }
        if (globalState.computedRequiresReaction) {
            console.warn(
                `[mobx] Computed value ${this.name_} is being read outside a reactive context. Doing a full recompute`
            )
        }
    }

    toString() {
        return `${this.name_}[${this.derivation.toString()}]`
    }

    valueOf(): T {
        return toPrimitive(this.get())
    }

    [Symbol.toPrimitive]() {
        return this.valueOf()
    }
}

五、整体步骤

1,创建reaction 对象

2,将获取observablevalue属性和值的方法传入到 reaction.track中,

3,调用reaction.track中trackDerivedFunction

4,在trackDerivedFunction 通过传入的f.call()发起收集依赖

5,调用observablevalue中的get方法,

6,通过Atom 中的 reportObserved方法将 observablevalue对象收集到 reaction.newObserving_队列中

7,使用绑定依赖方法 bindDependencies(derivation: IDerivation)将 创建的对象那个 reaction绑定到observablevalue对象的observers中

8, globalState.trackingDerivation 用于中间关联和暂存数据

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,492评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,048评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,927评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,293评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,309评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,024评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,638评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,546评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,073评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,188评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,321评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,998评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,678评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,186评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,303评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,663评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,330评论 2 358

推荐阅读更多精彩内容