核心机制及其实现

299 阅读19分钟

引子

考虑以下代码:

import { observable, autorun } from 'mobx';

const boxValueA = observable.box('A');
const boxValueB = observable.box('B');

autorun(() => {    // fnA
    console.log(boxValueA.get());
});

autorun(() => {    // fnB
    console.log(boxValueB.get());
});

console.log('set newA');
boxValueA.set('newA');


console.log('set newB');
boxValueB.set('newB');

控制台打印结果如下图所示:

机制

上述示例向我们展示了一次完整的响应式过程:1. 首先我们将数据A和数据B转换成了可观察的对象(Observable)boxValueA和boxValueB,这两个对象分别被autorun包裹的函数(这里记为fnA、fnB,见代码注释)所使用;2. 接下来两个autorun函数分别执行,执行过程中一并执行了其包裹的函数fnA和fnB,控制台打印出了A和B;3. 最后我们首先给boxValueA重新赋值,这时使用了boxValueA的函数fnA被自动触发执行了,控制台打印出了newA,接下来给boxValueB重新赋值,这时使用了boxValueB的函数fnB同样也被自动触发执行了,控制台打印出了newB。

上述过程描述了响应式的一个核心机制:在一个可观察对象(Observable)的值发生变更时,所有使用了该对象的函数,都会被自动触发执行。

问题

要实现上述机制,有一个核心问题需要解决:该Observable对象如何知道那些函数使用了它?

如果该Observable对象知道了哪些函数使用了它,那么事情就变得简单了:每次该Observable对象的值发生变更时,主动调用一下这些使用了它的函数就可以了。

但是细细想一想,该Observable对象好像无从得知哪些函数使用了它!在某个函数使用该Observable的值时,会触发该Observable对象的get方法,但是在该get方法的上下文执行环境中,并没有关于调用该方法的函数的任何信息,所以该Observable对于哪些函数使用了它这件事情无法感知。

那要怎么做呢?既然无法在get方法的上下文执行环境中感知到哪个函数调用了它,让它感知到就好了。

解决方案

如何让它感知到呢?直接运行这个函数看起来不行,如果运行在我们自定义的上下文环境中呢?

要是这个函数运行在我们自定义的上下文环境中,在执行它前,我们 “记住” 正在执行的是这个函数,而后执行该函数,执行期间其触发了Observable的get方法时,由于该get方法也运行在我们自定义的上下文环境中,所以它可以 “询问” 我们自定义的上下文环境当前执行的是哪个函数,我们将之前“记住”的这个函数告知它即可。

假设我们有一个Observable的对象ObservableA,和一个使用了该对象的函数fnA,用一张图来描述上述过程:

事实上,Mobx在JS运行的全局上下文(window或global)上定义了一个mobxGlobals的对象来作为实现上述“记住”和“被询问”的媒介,从而解决让Observable对象知道哪些函数使用了它这件事。值得一提的是,在源码中使用的是globalState这个变量,而挂载到window或global上的是mobxGlobals这个变量,为了语义化明确吧。下文中我们会统一使用globalState这个变量来进行表述,和源码保持一致。

源码实现过程

为了更好的看清楚发生了什么,先去掉上节ObservableB相关的代码,聚焦于ObservableA,如下所示:

import { observable, autorun } from 'mobx';

const boxValueA = observable.box('A');

autorun(() => {    // fnA
    console.log(boxValueA.get());
});

console.log('set newA');
boxValueA.set('newA');

这时控制台只打印出关于ObservableA相关的数据:

总领

用一幅图来描述源码所做的事情:

转变为Observable

首先,需要调用Mobx提供的函数Observable.box将我们的变量转化为一个Observable类型的对象,该对象封装了一系列和实现响应式及响应优化相关的属性和方法,比如observers ,value,diffValue_,isBeingObserved_,reportChanged()等等,为了更好的聚焦发生了什么,我们先关注和核心流程相关的属性及方法,定义ObservableValue的类:

class ObservableValue<T> {
    value_: T;
    observers_: Set<IDerivation>;
    reportObserved: () => boolean;
    reportChanged: () => void;
    set: (newValue: T) => void;
    setNewValue_: (newValue: T) => void;
    get: () => T;
    
    // 其值只有0和1,用来做依赖更新,下一章节会讲到
    diffValue_: number
    
    constructor(value: T) {
        this.value_ = value;
    }
}

Observable.box('A')即返回了一个new ObservableValue('A')的对象。

可以注意到observers_是一系列的Derivation,这些Derivation是什么?作者在源码的注释里给出的定义如下:A derivation is everything that can be derived from the state (all the atoms) in a pure manner。 derivation的本意是由来、派生,其核心的属性如下:

class IDerivation {
    // 上一次依赖的的Observable
    observing_: IObservable[];
    // 当前依赖的Observable
    newObserving_: null | IObservable[];
    // 运行过程中还未绑定的Observable数量
    unboundDepsCount_: number;
    // 为true时,该derivation的没有依赖任何Observable时log出警告
    requiresObservable_?: boolean;
    // 依赖发生变更时自动执行的函数(如上文fnA,不过被包了一些额外的代码)
    onBecomeStale_(): void;
}

可以看到,其属性值基本都与Observable有关系,从本质上来讲Derivation是由一系列的Observable及其衍生值组成的一个集合对象。从现实角度来看,这个集合对象是从可观察对象(Observable)到自动响应(Reactive)的一个桥梁。

转变Reaction

运行autorun的过程中,实际上做了两件事情:1. 将我们的响应函数(fnA)转变为Reaction;2.触发该Reaction的Schedule函数完成依赖收集。

首先来看看Reaction,和上述Observable一样,Mobx会将我们的响应函数(fnA)转变成一个Reaction的对象,该对象上封装了一系列和实现响应式及响应优化相关的属性和方法,其本身也实现了上Derivation的接口,同样的,让我们聚焦于核心属性及方法:

class Reaction implements IDerivation, IReactionPublic {}

class IReactionPublic {
    trace(): void;
}

class Reaction {
    // IDerivation
    // 上一次依赖的的Observable
    observing_: IObservable[];
    // 当前依赖的Observable
    newObserving_: null | IObservable[];
    // 运行过程中还未绑定的Observable数量
    unboundDepsCount_: number;
    // 依赖发生变更时自动执行的函数(如上文fnA,不过被包了一些额外的代码)
    onBecomeStale_(): void;
    
    // IReactionPublic
    // 在运行fnA前准备运行上下文,保证依赖可被收集到
    trace(): void;
    
    // Reaction
    // 在自定义上下文中运行fnA,保证依赖收集正确,批处理等等,内部会调用上述trace
    schedule_(): void;
    // 被this.track包裹的我们的fnA函数 -> () => this.track(fnA) 
    onInvalidate_: void;
    
    constructor(onInvalidate_) {
        this.onInvalidate_ = onInvalidate_;
    }
}

可以看到,Reaction实现了上述的IDerivation接口,因而内部保存了一系列的Observable及其衍生值,这些值构成了实现响应式的基础。除此之外,其还实现了IReactionPublic接口,接口内的trace方法在运行fnA之前,为fnA准备了自定义的运行环境(如上一章提到的通知__mobxGlobals),确保依赖可以被正确收集。

其自身还有一个schedule方法,除了调用trace外,还会做一些额外的事情,比如将当前执行的Reaction放到__mobxGlobals中、开启一个事务、计算该Reaction的依赖目前的状态等等(这部分特性是为了解决现实中遇到的问题而出现的,我们会在下一章详细说明)。

调用new Reaction(fnA)后,就将我们的fnA函数转变为一个具有响应式能力的Reaction了。

运行Reaction的Schedule

经过以上两步,将我们的数据转化为Observable的对象,将我们的fnA转化为Reaction的对象后,就可以进行依赖收集了。这一步主要为了获得2个信息:1 .使用某个Observable的observers有哪些;2.某个Reaction依赖的Observables有哪些。 前者可以使得某个Observable值发生变更时知道需要去自动触发哪些observers的调用,后者使得某个Observable中的observers保持最新(实际上Reaction中维护了observingnewObserving_两个Observable数组,每次执行依赖收集后,都会diff两者的区别,用结果来更新相关Observable中的observers信息,保证依赖是最新的)。

怎么获得上面的信息呢?核心在于上一节提到的需要利用一个执行上下文的媒介( globalState )来进行信息的交换和收集。 在运行fnA之前,告诉globalState当前运行的是fnA的Reaction,然后在运行fnA,运行过程中,触发了某个Observable的get时,该Observable从globalState拿到当前在执行的Reaction,将自己放到该Reaction的newObserving属性中,完成上述信息的收集。收集完毕后,进行一步bindDependencies的操作,通过diff当前Reaction中的observing和newObserving来得出该Reaction实际上依赖哪些Observable,从而更新这些Observable中的观察者observeing(关于如何做diff这点下一章节详细说明)。

接下来看一下源码中的实现过程:

上图中蓝色部分的函数调用是为了准备依赖收集前的环境,黄色部分真正进行依赖的收集。下来我们逐一看下这些函数做了哪些事情(为了更好的聚焦发生了什么,一些和核心流程无关的细节做一些忽略):

reaction.schedule_()

schedule_() {
    globalState.pendingReactions.push(this)
    runReactions()
}

可以看到,schedule函数主要是将当前的Reaction推入globalState的pendingReactions队列中,为什么不直接将当前的Reaction标记为正在执行的Reaction呢?这是因为在Reaction执行的过程中,可能还会触发其它的Reaction执行,所以需要将它们放到pendingReaction的队列中,依次执行。随后运行了公共的runReactions(这里的公共是指该方法是一个独立的函数,不是某个对象上的方法)方法。

runReactions()

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

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

可以看到,在runReactions的执行中有一个对globalState.inBatch和globalState.isRunningReactions的判断,关于这两个值及其作用,在下一节会详细说明,它们的主要目的是为了实现批处理。除此之外,runReaction就仅仅调用了reactionScheduler函数,从上述代码块中可知,这个函数做的事情仅仅是把传递给它的函数执行了一遍,那为什么还要单独声明一下嘞?可能只是为了规范传递给它的函数类型。那接下来看看runReactionsHelper函数做了什么事情。

runReactionsHelper()

/**
 * Magic number alert!
 * Defines within how many times a reaction is allowed to re-trigger itself
 * until it is assumed that this is gonna be a never ending loop...
 */
const MAX_REACTION_ITERATIONS = 100


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


    while (allReactions.length > 0) {
        if (++iterations === MAX_REACTION_ITERATIONS) {
            console.error(
                __DEV__
                    ? `Reaction doesn't converge to a stable state after ${MAX_REACTION_ITERATIONS} iterations.` +
                          ` Probably there is a cycle in the reactive function: ${allReactions[0]}`
                    : `[mobx] cycle in reaction: ${allReactions[0]}`
            )
            allReactions.splice(0)
        }
        let remainingReactions = allReactions.splice(0)
        for (let i = 0, l = remainingReactions.length; i < l; i++)
            remainingReactions[i].runReaction_()
    }
    globalState.isRunningReactions = false
}

纵观整个函数的实现,其所作的事情是依次触发globalState.pendingReactions队列中各个Reaction的runReaction的方法,为什么要用两层循环呢?因为在实际的执行Reaction的runReaction过程中,可能还会有新的Reactions被推到globalState.pendingReactions队列中等待执行,为了确保这些新推入的Reactions也能够执行,因而在每个批次执行完毕后都会看一下globalState.pendingReactions队列是否被清空,如果没有的话继续执行。

这样做有一个隐患,如果这些被执行的Reactions间有循环依赖的情况该怎么办?比如ReactionA内会将ReactionB丢到队列里,而ReactionB同样的也将ReactionA丢到队列里,这样就导致globalState.pendingReactions队列永远也无法清空了。为了预防这点且给出使用者提示,Mobx作者定义了一个魔法数字100(为啥是100可能是拍脑袋想的),当在runReactionsHelper函数内清空globalState.pengingReactions队列的次数达到这个阈值后,就打印出error的提示,告知使用者可能出现了循环执行依赖。

接下来看一看Reaction内的runReaction_方法做了哪些事情。

Reaction.runReaction_()

runReaction_() {
    startBatch()

    try {
        this.onInvalidate_()
    } catch (e) {
        this.reportExceptionInDerivation_(e)
    }
    
    endBatch()
}

同样地,为了聚焦于核心的实现流程,这里先将其它无关的代码删掉(如一些给spy的通知等等)。可以看到,主要的函数体主要做了2件事情:1. 在函数开始前后分别调用startBatch和endBatch;2. 在try-catch中调用this.onInvalidate函数。

startBatch和endBatch是什么东西?想象一个场景,如果在一个函数中修改ObservableA和ObservableB的值,那么同时依赖这两个Observable值的函数会被自动运行两次吗?从实际场景考虑,运行两次是不合理的,因为我们可以等它所依赖的值都修改完成后,运行一次即可,要不然只会徒增运行次数,增加性能损耗,还会产生一些不需要的中间态。那如何做到这一点儿呢,如何知道什么时候该去运行呢?这就涉及到transaction(事务)的概念及其实现了。Mobx提供了一个简单但有效的机制来实现事务,外在表现即为startBatch()和endBatch()两个函数,关于其详细原理及实现细节在下一节详细描述。

然后this.onInvalidate是什么呢?本文转变为Reaction一节有提到:被this.track包裹的我们的fnA函数 -> () => this.track(fnA) 有点儿绕,this.track又是什么?我们来看下this.onInvalidate的生成:

// view即为我们的fnA函数
export function autorun(view: (r: IReactionPublic) => any){
    // ......
    reaction = new Reaction(
        name,
        // 我们的this.onInValidate函数
        function (this: Reaction) {
            this.track(reactionRunner)
        },
        opts.onError,
        opts.requiresObservable
    )
    
    // 为了将生成的reaction当做参数传递给我们的fnA    
    function reactionRunner() {
        view(reaction)
    }
    
    // ......
}

可以看到,fnA即为view,源码在实际fnA运行过程中,会把生成的Reaction实例作为参数丢给fnA函数,使得我们在编写fnA函数时可以访问到生成的Reaction实例,为此源码定义了一个ReactionRunner函数,来协助运行过程中将Reaction实例传递给fnA函数,不过笔者很少遇到过在用autorun实现业务逻辑中需要拿到Reaction实例做一些事情的场景。

到这里就比较清晰了:this.onInvalidate函数即为被this.track包裹的传递了Reaction实例的fnA函数。接下来看下this.track函数做了哪些事情。

this.track()

track(fn: () => void) {
    startBatch()
    const result = trackDerivedFunction(this, fn, undefined)
    endBatch()
}

同样的,先开启了一个事务,在事务中运行一些方法。this.track剥去与核心实现无关的代码后,只是调用了公共的trackDerivedFunction方法,把当前的Reaction实例(this)和我们的fnA(fn)作为参数传递过去。

trackDerivedFunction()

export function trackDerivedFunction<T>(derivation: IDerivation, f: () => T, context: any) {
    derivation.newObserving_ = new Array(derivation.observing_.length + 100)
    derivation.unboundDepsCount_ = 0
    const prevTracking = globalState.trackingDerivation
    globalState.trackingDerivation = derivation
    let result
    try {
        result = f.call(context)
    } catch (e) {
        result = new CaughtException(e)
    }
    globalState.trackingDerivation = prevTracking
    
    // 更新Observable中的observer_及Reaction中的observering_
    bindDependencies(derivation)


    return result
}

到这一步就开始真正的收集依赖了,前面的函数调用都是为了收集依赖所做的准备工作。

如上文运行Reaction的Schedule一节所提到的,我们收集的依赖信息主要包括2点:1 .使用某个Observable的observers有哪些;2.某个Reaction依赖的Observables有哪些。 这些信息可以帮助我们实现响应式所具有的机制。 

我们会1. 先收集Reaction依赖的Observables;2.之后由Reaction的newObserving和observing来推导出使用某个Observable的observers有哪些。

首先初始化一下Reaction的newObserving_属性,长度为Reaction当前依赖的Observable数量加100,从源码中可以看到,Mobx的作者特别喜欢100这个数字,上文提到的为了防止Reaction循环执行所设置的执行阈值也为100;

然后初始化unboundDepsCount_值为0,该值在后续diff新旧依赖时会用到;

接下里就是将globalState的当前执行的Reaction设置为传进来的Reaction,这样在后续运行fnA时触发所用的Observable的get方法里就可以知道当前执行的Reaction是哪个了。可以看到,执行完毕后,还需要将当前执行的方法恢复为上一次执行的方法,因为在现实的场景中,总会遇到Reaction执行嵌套的情况,这样就模拟了一个调用栈,确保每个Reaction的依赖收集正确。

最后是bindDependencies,这一步会计算出某个Observable的observres有哪些。

先来看看如何收集Reaction依赖的Observables有哪些,在上述Reaction及globalStates的变量初始化完成后,调用了我们的fnA方法(f.call(context)),我们的fnA定义为:

autorun(() => {    // fnA
    console.log(boxValueA.get());
});

里面调用了Observable的变量boxValueA的get方法,拿到其值,并且打印了出来。在调用Observable的get方法时,会将其自身放进Reaction的依赖的Observables中,我们看一下get方法具体做的事情:

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

get方法内仅仅调用了其自身实例的reportObserverd方法,然后将当前值返回出去。其实例上的reportObserved方法做了哪些事情呢?

public reportObserved(): boolean {
    return reportObserved(this)
}

可以看到,仅仅只是将其自身实例作为参数调用了公共的reportObserved方法,那reportObserved方法做的事情呢?

export function reportObserved(observable: IObservable): boolean {
    const derivation = globalState.trackingDerivation
    if (derivation !== null) {
        derivation.newObserving_![derivation.unboundDepsCount_++] = observable
        return true
    }
    return false
}

啊哈!在去掉一些和性能优化、spy通知相关的代码后,可以看到它做的事情很直观:拿到咋们在执行fnA前预先放在全局对象globalState上Reaction,然后将传递过来的Observable对象丢进它的newObserverving_数组中。这样就将执行fnA时所依赖的Observable(在咋们的例子中是boxValueA)收集了起来。‌

执行完成后函数依次出栈,console.log也如愿拿到了boxValueA的值'A'并将其打印了出来:

至此我们完成了第一件事情:某个Reaction依赖的Observables有哪些。 这个Reaction依赖的Observables被放在了Reaction的newObserving_数组中。

那第二件事情使用某个Observable的observers有哪些呢?这部分是在bindDependencies函数中完成的。文中在提到bindDependencies时会伴随着提到diff,那么diff究竟是什么?为什么要做diff?

考虑一个场景,如果我们的函数fnA在条件判断中使用Observable的值,那么在判断为false时就无法知道fnA依赖了Observable这件事了;或者判断一开始为真,下一次运行为假,那么fnA在此时就没有对Observable的依赖了,Observable的值更新后不应该自动的触发fnA,用以下代码诠释:

let condition;

// 情况一
autorun(() => {    // fnA
    // 1. 假设condition这时候为假,我们就收集不到boxValueA的依赖了
    // 2. 下一次执行fnA时假设condition条件为真,我们又希望收集到关于boxValueA的依赖
    if (conditione) {
        console.log(boxValueA.get());
    }
});

// 情况二
autorun(() => {    // fnA
    // 1. 假设在auton的时候condition为真,收集到了boxValueA的依赖
    // 2. boxValueA的值更新,触发fnA重新执行
    // 3. 重新执行时候假设condition为假,不会触发对boxValueA的依赖
    // 4. 这时候表明fnA已经对boxValueA没有依赖了,下次boxValueA的值更新后不应该在自动触发fnA
    // 5. 我们需要某种机制来移除boxValueA中的观察者fnA
    if (conditione) {
        console.log(boxValueA.get());
    }
});

 上述代码只诠释了比较直观的两个例子,现实场景往往更加复杂,这就要求我们不仅得到使用某个Observable的observers有哪些,而且需要保证这条信息永远最新。 因而需要在每次运行fnA收集到当前的最新依赖值newObserving_,收集完成后和上一次的的observing_做一个diff,拿到:1. 当前Reaction需要被添加到哪些Observable的observers_中;2. 当前Reaction需要从哪些Observable的observers_中移除。 这两个信息。拿到这两个信息后,更新相应Observable的observers_,就可以完成使用某个Observable的observers有哪些这条依赖的收集了。

bindDependencies正是做了这件事情,根据Reaction的observing_和newObserving_计算出上述信息,而后更新相应的Observable的观察者数组observers_。关于bindDependencies是如何完成上述事情的,将在下一章中详细说明。

至此,我们所需要的依赖就收集完毕了,接下来看一看Mobx是如何用他们来完成响应式的。

改变Observable的值

经过上述将value转变为ObservableValue、fnA转变为Reaction、依赖收集后,在为boxValueA设置新的值时,就会触发对boxValueA有依赖的函数fnA自动执行了。

boxValueA.set('newA');

来看看set函数都做了哪些事情:

public set(newValue: T) {
    const oldValue = this.value_
    newValue = this.prepareNewValue_(newValue) as any
    if (newValue !== globalState.UNCHANGED) {
        this.setNewValue_(newValue)
    }
}
    
private prepareNewValue_(newValue): T | IUNCHANGED {
    newValue = this.enhancer(newValue, this.value_, this.name_)
    return this.equals(this.value_, newValue) ? globalState.UNCHANGED : newValue
}


// 默认的equals函数
function defaultComparer(a: any, b: any): boolean {
    return Object.is(a, b)
}

可以看到,set函数主要是调用prepareNewValue_预先对新的value值做一个处理,要是值和上一次不同的话,则运行setNewValue。其中prepareNewValue_中用到的enhancer和equals函数,都是Observable的构造参数,它们的默认值分别为返回当前value_和Object.is。之所以把这些运行时的函数作为可配置的,目的在于提供更定制化的修改和比较能力,使得代码编写更加优雅。

如一个实际的例子:向后端发送一个长轮询的任务,每隔一段时间向后端请求一次状态,后端会根据任务情况返回状态码:start(开始)、pending(等待中)、running(执行中)、success(成功)、fail(失败),这些状态体现在用户界面上可能只有loading、success和fail,这时候我们就可以为这个状态的Observable定义一个equals,其中判断start、pengding和running的值是一样的,状态值在这三个值之间变化时,就不会触发更新了。

接下来看一下this.setNewValue_做了哪些事情:

setNewValue_(newValue: T) {
    this.value_ = newValue
    this.reportChanged()
}

this.setNewValue首先将自己的value设置为新传进来newValue,而后调用了this.reportChanged函数来通知自己的的值更新了这件事。

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

this.reportChanged函数做的事情也很直观:开启一个事务,而后在其中将自身传递给公共的propagateChanged函数进行调用。那么propagateChanged函数做了哪些事情呢?

export function propagateChanged(observable: IObservable) {
    // Ideally we use for..of here, but the downcompiled version is really slow...
    observable.observers_.forEach(d => {
        d.onBecomeStale_()
    })
}

啊哈!可以看到它依次调用了所有观察者,也就是Reaction的onBecomeStale方法,不难看出,这个方法应该就是自动触发Reaction自动执行的关键。如上代码块中所示,作者在这行源码中给出了注释// Ideally we use for..of here, but the downcompiled version is really slow...,这里比较理想的写法是用for...of,但是编译出来的代码执行效率不高,所以换用了forEach,不过v8等引擎对这些常用的方法做了额外的性能优化,所以有时候用这些函数式的写法效率反而更高。 那onBecomeStale做了什么事情呢?看一下他的实现:

onBecomeStale_() {
    this.schedule_()
}

这里笔者没有剔除任何其它代码,源码就只有这一行!而这个函数做的事情上面做了详细的诠释。

可以看到,它又走了一遍schedule的流程:1. 先准备运行Reaction的环境 、2. 执行Reaction的view(在例子中即为fnA函数)、3. 运行过程中触发Observable的get函数收集newObserving_的依赖、4.用bindDependencies计算出Observable的observers_,并更新。

为什么依赖不能只收集一次,之后Observable的值发生改变直接执行Reaction的view(fnA)呢?如前文提到的,我们需要保持依赖是最新的。实际编码中会存在很多条件判断等现实场景,这就导致我们的依赖会随着view函数的执行一直在动态的变化,因而每次触发自动更新时,都需要重新计算依赖。这样做的好处是可以让我们在编码过程中不需要关心太多额外的东西,专注于编码即可。 当然,作为偏底层的框架,性能相关的因素是非常值得考虑的,Mobx也做了很多提高性能的设计,不过这一章节为了聚焦于它的核心流程,因而将这些部分省略掉了,下一章节我们会详细描述。除此之外,本章只讲述了Mobx的最基础的Observable类型:box,作为理解Mobx内部的核心实现,它是最为直观的。但是在现实场景中我们用到的很少,因为它的数据类型不足以描述实际业务场景所需的数据结构,因而Mobx提供了更加丰富的Observable类型,如Object,Array等等,它们的底层实现机制和本章所讲述的一致,只是为了支持自身数据类型的种种性质做了一些其他事情,在后面的章节也会详细说明。

至此,我们已经了解了Mobx关于响应式的核心实现机制及其源码实现过程,下一章我们看一看为了应对现实的场景,Mobx做了哪些额外的实现补充吧。