从源码出发浅析mobx依赖收集和依赖更新原理

2,099 阅读1分钟

先看下面的代码:

class Demo {
  @observable
  public test = 1

  log: () => void = autorun(() => {
    console.log(`test input onChange: ${this.test}`)
  })
}

修改test的值,会触发log函数自动执行。相当于传入autorun的方法,会自动收集依赖到的 observable值的变化。个人猜测autorun函数的工作方式是这样的

function autorun (fn) { 
   // 依赖收集的准备工作
   fn() // 触发observable属性的get方法
   // 清理依赖收集
}

下面来简单看一下autorun源码 (5.15.4)

function autorun(
    view: (r: IReactionPublic) => any,
    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
    
    // 只看同步的autorun,异步是根据传入的delay setTimeout
    if (runSync) {
        // normal autorun
        reaction = new Reaction(
            name,
            // reaction的onInvalidate, 用track调用reactionRunner, 也就是view(reaction), (重新)收集依赖
            function(this: Reaction) {
                this.track(reactionRunner)
            },
            opts.onError,
            opts.requiresObservable
        )
    } else {
       // ... 处理异步
    }

    function reactionRunner() {
        view(reaction)
    }
    // 将 reaction 放进全局 globalState.pendingReactions 队列,里面会执行runReactions
    reaction.schedule()
    // 返回取消订阅
    return reaction.getDisposer()
}

再来看看runReactions,runReactions是依赖收集启动方法

let reactionScheduler: (fn: () => void) => void = f => f();

function runReactions() {
  // 不在事务中并且没有正在执行的reaction
  if (globalState.inBatch > 0 || globalState.isRunningReactions) return
  // 核心的调用runReactionsHelper
  reactionScheduler(runReactionsHelper)
}

runReactionsHelper:

function runReactionsHelper() {
    globalState.isRunningReactions = true
    const allReactions = globalState.pendingReactions
    let iterations = 0

    // 遍历所有globalState.pendingReactions中的reaction,并执行每个对象的runReaction
    while (allReactions.length > 0) {
        if (++iterations === MAX_REACTION_ITERATIONS) {
            console.error(
                `Reaction doesn't converge to a stable state after ${MAX_REACTION_ITERATIONS} iterations.` +
                    ` Probably there is a cycle in the reactive function: ${allReactions[0]}`
            )
            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
}

runReaction关键就是触发onInvalidate参数函数, 也就是用track包裹的view函数(autorun的传入函数)

    // fn 就是view 函数
    track(fn: () => void) {
   
        startBatch()
        ....
        this._isRunning = true
        // trackDerivedFunction是核心, 把fn传入了trackDerivedFunction,
        const result = trackDerivedFunction(this, fn, undefined)
        this._isRunning = false
        ....
        endBatch()
    }

终于到trackDerivedFunction了,trackDerivedFunction就是最终调用autorun的传入函数的方法。至此就完成了触发observable属性的get方法。后面就是监听observable属性的get方法的调用, 最终完成依赖收集。例如在mobx ObservableValue类中有一个get方法,这个方法就是trap了observable属性的get方法:

  public get(): T {
        // reportObserved将 observable 上报给正在收集依赖的 derivation (reaction)
        //  derivation 从globalState.trackingDerivation中获取,globalState.trackingDerivation在上面提到的最终触发autorun的传入 
       //    函数 的 trackDerivedFunction中设置的
        this.reportObserved()
        return this.dehanceValue(this.value)
    }

至此完成了依赖收集。 下面来简单的说一下obserable属性的set方法,触发set方法,如果值改变,mobx会通知此obserable属性的依赖:

    public set(newValue: T) {
        const oldValue = this.value
        newValue = this.prepareNewValue(newValue) as any
        if (newValue !== globalState.UNCHANGED) {
             ...
            //值改变, 触发setNewValue, 最终会触发Reaction 中的onBecomeStale, 而onBecomeStale调用的就是 this.schedule(),这个就合前面的依赖收集重合了
            this.setNewValue(newValue)
            if (notifySpy && process.env.NODE_ENV !== "production") spyReportEnd()
        }
    }

最后,我们也可以看到mobx抽离Reaction这一层,设计的很巧妙,不仅抽离了依赖收集的逻辑,管理全局的依赖管理,也抽离了不同依赖管理阶段side effect,在初始化依赖收集的时候可以设置track get 方法,在依赖更新的时候也可以将Reaction用作他处,比如React,进行组件的更新。貌似Vue3抽离出的Reactivity也是类似的设计思想。