Mobx——derivation 的依赖收集过程 (离职拷贝版)

离职了,把 2019 年在公司写的文档 copy 出来。年头有点久,可能写的不太对,也不是很想改了~
注:本文档对应 mobx 版本为 4.15.4、mobx-vue 版本为 2.0.10

源码解析

1. autorun & reaction

先看一下 autonrun 和 reaction 的源码

function autorun(
    view: (r: IReactionPublic) => any,
    opts: IAutorunOptions = EMPTY_OBJECT
): IReactionDisposer {
    ...
    if (runSync) {
        reaction = new Reaction(
            name,
            function(this: Reaction) {
                this.track(reactionRunner)
            },
            opts.onError,
            opts.requiresObservable
        )
    } else {
        const scheduler = createSchedulerFromOptions(opts)
        // debounced autorun
        let isScheduled = false

        reaction = new Reaction(
            name,
            () => {
                if (!isScheduled) {
                    isScheduled = true
                    scheduler(() => {
                        isScheduled = false
                        if (!reaction.isDisposed) reaction.track(reactionRunner)
                    })
                }
            },
            opts.onError,
            opts.requiresObservable
        )
    }

    function reactionRunner() {
        view(reaction)
    }

    reaction.schedule()
    return reaction.getDisposer()
}


function reaction<T>(
    expression: (r: IReactionPublic) => T,
    effect: (arg: T, r: IReactionPublic) => void,
    opts: IReactionOptions = EMPTY_OBJECT
): IReactionDisposer {

    // 回调 effect 用 action 封装了一层
    const effectAction = action(
        name,
        opts.onError ? wrapErrorHandler(opts.onError, effect) : effect
    )

    const r = new Reaction(
        name,
        () => {
            if (firstTime || runSync) {
                reactionRunner()
            } else if (!isScheduled) {
                isScheduled = true
                scheduler!(reactionRunner)
            }
        },
        opts.onError,
        opts.requiresObservable
    )

    function reactionRunner() {
        isScheduled = false
        if (r.isDisposed) return
        let changed = false
        r.track(() => {
            const nextValue = expression(r)
            changed = firstTime || !equals(value, nextValue)
            value = nextValue
        })
        if (firstTime && opts.fireImmediately!) effectAction(value, r)
        if (!firstTime && (changed as boolean) === true) effectAction(value, r)
        if (firstTime) firstTime = false
    }

    r.schedule()
    return r.getDisposer()
}

其实就是 new 了一个 Reaction,只不过 autorun 把回调直接传进去 track 了,用起来比较无脑,而 reaction 则是把触发条件传进去 track,而回调则是用 action 包了一层,并且多一个 firstTime 参数来限制回调只能执行单次

这样带来的区别是什么?更加语义化的触发时机 + action 事务特性加持的回调函数

所以这里只需要搞懂 Reaction 就行了

2. Reaction

class Reaction implements IDerivation, IReactionPublic {
    observing: IObservable[] = []
    newObserving: IObservable[] = []
    dependenciesState = IDerivationState.NOT_TRACKING
    diffValue = 0
    runId = 0
    unboundDepsCount = 0
    __mapid = "#" + getNextId()
    isDisposed = false
    _isScheduled = false
    _isTrackPending = false
    _isRunning = false
    isTracing: TraceMode = TraceMode.NONE

    constructor(
        public name: string = "Reaction@" + getNextId(),
        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()
        }
    }

    isScheduled() {
        return this._isScheduled
    }

    runReaction() {
        if (!this.isDisposed) {
            startBatch() // globalState.inBatch++
            this._isScheduled = false
            if (shouldCompute(this)) {
                this._isTrackPending = true

                try {
                    this.onInvalidate() // 调用入参回调函数,也就是this.track(incomeFunction)
                    if (
                        this._isTrackPending &&
                        isSpyEnabled() &&
                        process.env.NODE_ENV !== "production"
                    ) {
                        // onInvalidate didn't trigger track right away..
                        spyReport({
                            name: this.name,
                            type: "scheduled-reaction"
                        })
                    }
                } catch (e) {
                    this.reportExceptionInDerivation(e)
                }
            }
            endBatch()
        }
    }

    track(fn: () => void) {
        startBatch()
        const notify = isSpyEnabled()
        let startTime
        if (notify) {
            startTime = Date.now()
            spyReportStart({
                name: this.name,
                type: "reaction"
            })
        }
        this._isRunning = true
        
        // 依赖收集的核心
        const result = trackDerivedFunction(this, fn, undefined)

        this._isRunning = false
        this._isTrackPending = false
        if (this.isDisposed) {
            clearObserving(this)
        }
        if (isCaughtException(result)) this.reportExceptionInDerivation(result.cause)
        if (notify && process.env.NODE_ENV !== "production") {
            spyReportEnd({
                time: Date.now() - startTime
            })
        }
        endBatch()
    }
    ...
}

然后追一下 reaction(expression, effect, opts) 里的调用顺序

  1. 利用 action 和 第二个入参 effect (待执行的回调函数)定义 effectAction
  2. const r = new Reaction
  3. r.schedule 也就是 Reaction.schedule
  4. Reaction.runReaction
  5. this.onInvalidate, 这里 onInvalidate 是 new Reaction 里 第二个参数,传进来的是 r.track()
  6. track 里面的核心内容是trackDerivedFunction,具体源码如下:

3. trackDerivedFunction

trackDerivedFunction(this, fn, undefined) 这里 fn 就是 reaction 里 第一个入参(依赖相关的条件函数) expression 对应的操作:

const nextValue = expression(r)
changed = firstTime || !equals(value, nextValue)
value = nextValue

// 追下 trackDerivedFunction 都干了啥
function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
    const prevAllowStateReads = allowStateReadsStart(true)
    changeDependenciesStateTo0(derivation)
    derivation.newObserving = new Array(derivation.observing.length + 100)
    derivation.unboundDepsCount = 0
    derivation.runId = ++globalState.runId
    const prevTracking = globalState.trackingDerivation

    // 用 Vue 的思路来理解这句话的意义大概就是当前的 Watcher 是这个 Reaction,然后触发的 Dep 会塞到这个 Watcher 里,这里是个暂时借用,get完之后又还回去了
    globalState.trackingDerivation = derivation
    let result

    // 这里回调f被执行掉了,触发变量的 get、获取 dep
    if (globalState.disableErrorBoundaries === true) {
        result = f.call(context)
    } else {
        try {
            result = f.call(context)
        } catch (e) {
            result = new CaughtException(e)
        }
    }
    globalState.trackingDerivation = prevTracking

    // 建立 dep 与 derivation 的依赖关系
    bindDependencies(derivation)

    warnAboutDerivationWithoutDependencies(derivation)
    allowStateReadsEnd(prevAllowStateReads)
    return result
}

4. bindDependecies

function bindDependencies(derivation: IDerivation) {
    
    const prevObserving = derivation.observing
    const observing = (derivation.observing = derivation.newObserving!)
    let lowestNewObservingDerivationState = IDerivationState.UP_TO_DATE

    let i0 = 0,
        l = derivation.unboundDepsCount
    for (let i = 0; i < l; i++) {
        const dep = observing[i]
        if (dep.diffValue === 0) {
            dep.diffValue = 1
            if (i0 !== i) observing[i0] = dep
            i0++
        }

        if (((dep as any) as IDerivation).dependenciesState > lowestNewObservingDerivationState) {
            lowestNewObservingDerivationState = ((dep as any) as IDerivation).dependenciesState
        }
    }
    observing.length = i0

    derivation.newObserving = null

    l = prevObserving.length
    while (l--) {
        const dep = prevObserving[l]
        if (dep.diffValue === 0) {
            removeObserver(dep, derivation)
        }
        dep.diffValue = 0
    }

    while (i0--) {
        const dep = observing[i0]
        if (dep.diffValue === 1) {
            dep.diffValue = 0
            // 这个就是把 dep 和 Reaction 建立联系,observable.observers.add(node),这个 node 是 Reaction 就好像把 Watcher 绑到 dep 的 target 上
            addObserver(dep, derivation)
        }
    }

    if (lowestNewObservingDerivationState !== IDerivationState.UP_TO_DATE) {
        derivation.dependenciesState = lowestNewObservingDerivationState
        derivation.onBecomeStale()
    }
}

看到这里 derivation.observing 也就有东西了,增加了对应的 dep 依赖关系。

mobx 的依赖关系是这样的,拿 Reaction 举例 Reaction.observing 下面是对应的 dep,这层关系就是我该被谁的变动唤醒,dep 下面的 observers 就是对应的 derivation: IDerivation(Reaction 或 ComputedValue),这层关系就是,我变动了应该去通知谁做后续的响应。对应 vue 就是 Watcher 的 Deps 里面是 dep、dep 的 target 和 subs 里面是 Watcher

结论

逻辑上真的和 Vue 神似,但是给我的直观感受就是(其实我对语义化的反应很迟钝):

因为主要接触的 vue 是 js 的 vue,里面提供一大类功能的东西没有这么细的拆分,比如 IListenable 接口对应一堆类,用一个 demo 一点一点跟的时候,思路单一,感觉不到很庞大,但是有的时候,比如 observers 在我的 demo 里就对应 Reaction ,但是他也可能对应 ComputedValue ,二者之间的逻辑并不是完全一致的,都有各自的操作,虽然 vue 里面 computed 创建的 Watcher 和其他 Watcher 派发更新时的逻辑也完全不同,但是他们都是 Watcher,虽然读起来没有区别,只不过是初始化参数的差异,但是意义十分明确,就是我是 一个Watcher。

反观 ComputedValue:

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

因为在派发更新的触发时 ComputedValue 和 ObservableValue 并列(这层关系没看懂有什么意义,因为 computedValue 一般都是只读的?)

所以把它塞在 ObservableObjectAdministration 的 value 下面,什么时候才能出发 set 操作?

但是在依赖收集的过程和派发更新执行时,他又和 Reaction 并列,从语义上直接读,只会感觉他和 ObservableValue 有关系,却读不出来 Reaction 和 ComputedValue 都是 IDerivation,就会感觉很蒙。

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

推荐阅读更多精彩内容