The most straightforward Mobx responsive mechanism

I. Introduction

mobx is a popular state management library today. Students who have used mobx know that it is a responsive update data, that is to use Object.defineProperty to proxy the processing of get and set. When we trigger get, we will collect dependencies and use set When modifying, it will notify some dependencies of the lawsuit to update, which is very similar to the responsive mechanism of vue2, which gives it a better advantage in performance, because based on responsive updates, the dependencies that need to be updated can be accurately notified. This article will analyze the principle of mobx responsive mechanism implementation from the source code level, and finally implement a responsive mechanism through a demo.

2. Handling responsive data

This example will be explained below:

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()

As mentioned earlier, mobx manages state based on responsive objects, so the organization state is in the form of classes. In Store, we use the makeObservable method to create a responsive proxy. At the same time, we also create a side effect, which will be generated when the count changes. is triggered.

1、MakeObservable

First of all, mobx will do a reactive proxy for the object. Let's take a look at what the object after the proxy looks like:

image.pngAs shown in the figure, mobx delegates the get and set methods of count, and their implementations become this[$mobx].getObserVablePropValue and setObservaBlePropValue, and our addCount method has also been processed, as shown below:

image.pngAll the methods we define will become executeAction, in this method we can do some optimization operations, such as batch update.

In addition to proxying our properties and methods, the makeAutoObservable method will also create a new $mobx property on the object. This property is an ObservableObjectAdministrationinstance on which the original object that needs to be processed, namely target_, needs to become reactive The attribute is values_, and the proxy of the set and get methods of the attribute is also on this instance. Let's understand his implementation through the source code.

// 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 的优化点的实现。

Guess you like

Origin juejin.im/post/7118555535732899871