最通俗易懂的Mobx响应式机制

一、前言

mobx 是当今流行的状态管理库,用过 mobx 的同学就知道它是响应式的更新数据,也就是使用 Object.defineProperty 代理 get、set 的处理,当我们触发 get 时就会去收集依赖,通过 set 去修改时会通知诉讼有的依赖做更新,这和 vue2 的响应式机制很类似,这就让它在性能上有比较好的优势,因为基于响应式更新可以精准的通知需要更新的依赖,本篇文章将会从源码层面去分析 mobx 响应式机制实现的原理,最后会通过一个 demo 简单实现一个响应式机制。

二、处理响应式数据

下面将会以这个例子进行讲解:

class Store {
    count = 1;
    addCount() {
        this.count++
    }
    constructor() {
        makeObservable(this, {
            count: observable,
            addCount: action
        })
        autorun(() => {
            console.log(this.count)
        })
    }
}

const store = new Store();
store.addCount()

前面介绍过 mobx 是基于响应式对象来管理状态,所以组织状态是用 class 的形式,在 Store 中我们通过 makeObservable 方法来创建响应式的代理,同时我们还创建了一个副作用,当 count 发生变化时会被触发。

1、MakeObservable

首先 mobx 会对对象做响应式代理,我们来看一下代理之后的对象长什么样:

image.png 如图所示,mobx 对 count 的 get 和 set 方法进行了代理,他们的实现变成了 this[$mobx].getObserVablePropValue 和 setObservaBlePropValue,我们的 addCount 方法也被做了一层处理,如下所示:

image.png 我们定义的所有的方法都会变成 excuteAction ,在这个方法中我们可以做一些优化操作,比如批量更新。

makeAutoObservable 方法除了给我们的属性和方法进行代理外还会在对象上新建一个 $mobx 的属性,这个属性是一个 ObservableObjectAdministration 实例,在这个实例上保存了需要被处理的原始对象即 target_、需要变成响应式的属性即 values_,属性的 set 和 get 方法的代理也在这个实例上,下面我们通过源码来了解他的实现。

// makeObservable.ts
export function makeObservable(target, annotations, options) {
    // 创建一个 ObservableObjectAdministration 实例,将该实例绑定在对象的$mobx属性上
    const adm =  asObservableObject(target, options)[$mobx];
    // 对需要变成响应式的属性和方法进行处理
    ownKeys(annotations).forEach(key => adm.make_(key, annotations[key]))
}

makeObservable 方法的代码非常简短,他就干了两件事,创建 ObservableObjectAdministration 实例 $mobx,然后遍历需要变成响应式的属性和方法,然后调用 $mobx 上面的 make_ 方法,对这些属性的 set 和 get 方法进行代理,对于方法来说会被处理成 excuteAction,我们具体来看下 make_ 方法:

    // key: count
    // annotation: observable
    make_(key, annotation) {
        let source = this.target_;
        // 沿着原型链去遍历属性,对属性进行处理
        while(source && source!==Object.prototype) {
            const descriptor = Object.getOwnPropertyDescriptor(source, key);
            if(descriptor) {
                // this:ObservableObjectAdministration
                // key: count
                // descriptor: {enumerable: true,value: 1,writable: true }
                annotation.make_(this, key, descriptor, source);
            }
            source = Object.getPrototypeOf(source)
        }
    }

make_ 方法会沿着原始对象的原型链去找需要处理的属性和方法,因为方法一般是存在于对象的原型上,然后拿到这个属性的描述对象,最终调用 annotation(属性/方法注册时对应的值,比如count注册时被赋予observable)的 make_ 方法对其进行处理,下面我们来看一下 observable 是怎么处理 count 属性的。

注意点: 这里要区分一下这两个make_方法,$mobx 上面的 make_ 方法相当于一个入口方法,他接受一个参数 annotation,表示属性的类型,一般来说 annotation 可以是 action、observable、computed等,他们都是去处理不同场景下的数据,在 annotation 上会有自己的 make_ 方法,去处理对性的数据。

2、observable.make_

function make_(adm, key, descriptor) {
     return adm.defineObservableProperty_(
        key,
        descriptor.value,
        // 默认采用深度遍历的方法去处理key对应的值,因为有可能这个值是一个比较深的对象,
        // 那我们就需要处理这个对象的每一个值
        this.options_?.enhancer ?? deepEnhancer, 
        proxyTrap
    )
}

其实这个方法最终还是去调用 $mobx 上的 defineObservableProperty_ 方法,这个方法就会为 count 属性创建一个 ObservableValue 实例,这个我们可以从上面的图中可以发现,然后会以 key-value 的形式保存在一个 Map 中,即 $mobx 中的 values_ 属性,也就是说 $mobx.values_ 上保存了所有的响应式数据,这样就能方便我们快速找到某个属性对应的观察者对象,我们看下这个方法的实现:

defineObservableProperty_(key, value, enhancer, proxyTrap) {
    // 获取该属性的set和get方法的代理方法
    const cachedDescriptor = getCachedObservablePropDescriptor(key)
    // 重新生成属性的描述对象
    const descriptor = {
        configurable: true,
        enumerable: true,
        get: cachedDescriptor.get,
        set: cachedDescriptor.set
    }
    Object.defineProperty(this.target_, key, descriptor);
    // 为该属性创建一个ObservableValue实例
    const observable = new ObservableValue(
        value,
        enhancer,
        `${this.name_}.${key.toString()}`,
        false
    )
    // 将key和observable保存在value_中
    this.values_.set(key, observable)
}

下面回顾一下上面的流程:

image.png

三、收集依赖和副作用

在 Mobx 中有几种创建副作用的 API,比如 autorun、when、reaction,当他们依赖的属性发生变化时就会自动执行副作用,下面主要介绍 Mobx 是如何收集副作用并进行响应式更新的,下面以 autorun 为例进行讲解:

export function autorun(view) {
    const name = "Autorun@" + getNextId();
    // 创建一个副作用
    const reaction = new Reaction(
        name,
        function () {
            this.track(reactionRunner)
        }
    )
    function reactionRunner() {
        view(reaction);
    }
    // 调度reaction
    reaction.schedule_();
}

在 autorun 中主要做了两件事,第一是创建当前 autorun 的副作用 Reaction,然后就是调度当前这个副作用,在这个副作用中会去执行一次我们传入的回调函数,因为在执行回调函数的过程中就会触发属性的 get 方法,这样我们就能将当前的副作用添加到属性的副作用队列中。

下面我们就沿着这个方向去看一下属性的 get 方法:

// 首先触发的是$mobx上的代理方法
// $mobx.getObservablePropValue_
getObservablePropValue_(key) {
    // 首先获取属性对应的ObservableValue实例,在调用实例上的get方法,因为属性的值都存在这个实例中
    return this.values_.get(key).get();
}
 
// ObservableValue实例上的get方法
get() {
    // 发送通知,需要进行依赖的收集了,此处是通知Reaction去收集其依赖的属性
    this.reportObserved()
    return this.value_
}

// 最终调用的方法,这里的参数observable指的就是当前属性对应的ObservableValue实例
export function reportObserved(observable) {
    // globalState.trackingDerivation中缓存的是当前正在进行的Reaction,当我们在执行Reaction的
    // 调度时就会将当前这个Reaction保存在全局对象中
    const derivation = globalState.trackingDerivation;
    if(derivation!==null) {
        // 将当前的ObservableValue实例保存在Reaction的newObserving_队列中
        derivation.newObserving_[derivation.unboundDepsCount_++] = observable
    }
}

Reaction 的依赖收集就介绍完了,下面就是副作用的收集,副作用的收集发生在依赖收集的后面,此时当前 Reaction 的所有依赖收集完成了那么给属性添加副作用不就简单了,只需要遍历一下 newObserving_ 队列就行了,我们来看下:

function bindDependencies(derivation) {
    // 拿到收集到的依赖队列
    const observing = derivation.newObserving_;
    // 拿到收集到的依赖的数量
    const l =  derivation.unboundDepsCount_;
    let i0 = 0;
    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++
        }
    }
    // 遍历队列
    while (i0--) {
        // 拿到对应的依赖,即属性对应的ObservableValue实例
        const dep = observing[i0]
        if (dep.diffValue_ === 1) {
            dep.diffValue_ = 0
            // 往ObservableValue实例的observers_队列中添加副作用
            dep.observers_.add(derivation)
        }
    }
}

现在完整的依赖和副作用的收集就介绍完了,我们再来复习一下:

image.png

四、响应式更新数据

现在我们已经收集完属性的副作用,那门当我们更新属性的值的时候就需要通知这些副作用重新执行,这当然就要在属性的 set 方法中去实现,我们来看一下具体的实现:

// 首先触发的是$mobx上的代理方法setObservablePropValue_
setObservablePropValue_(key, newValue) {
    // 获取属性对应的ObservableValue实例
    const observable = this.values_.get(key)
    // 调用实例中的setNewValue_方法更新值
    observable.setNewValue_(newValue);
}

// 更新完值之后最终会执行这个方法
export function propagateChanged(observable) {
    observable.observers_.forEach(d => {
        // 调用副作用的onBecomeStale_方法,在这个方法中会重新调度一下副作用
        d.onBecomeStale_()
    })
}

响应式更新数据的基本实现就介绍到这里,这一块其实还有许多细节需要考虑,比如数据的批量更新、数据的缓存优化等。

最后我写了一个 mobx 响应式的简单版本,欢迎评论区留言~,后面还会继续加更 mobx 的优化点的实现。

猜你喜欢

转载自juejin.im/post/7118555535732899871
今日推荐