浅探 Mobx 的函数式响应编程

950 阅读7分钟

前言

  • 提起 Mobx 想必大家已经很熟悉了,在它的官方介绍中有这么一句话叫做“MobX 是一个身经百战的库,它通过运用透明的函数式响应编程使状态管理变得简单和可扩展。”,今天这篇文章让我们一起探索下它的函数式响应编程到底是如何实现的。
  • 借用官网的一张图我们可以看到 Mobx 是采用的单向数据流,利用 Action 改变 Observable State ,再通过执行 Reaction 副作用函数, 从而更新所有受影响的 View 。 image.png
  • 我们重点从上方图片中提到的两个核心概念入手,一是 Observable State(响应式数据),二是 Reactions(响应式数据变更后执行的副作用函数)。

Observable State(响应式数据)

  • 我们先从 Observable State 响应式数据入手,看下 Mobx 是如何将原始数据处理为响应式数据的。在 Mobx 中创建响应式数据的方式有很多,像 makeObservablemakeAutoObservableobservable,但是他们实现逻辑是一致的。
  • 现在我们以 observable 这个 api 作为入口示例,它支持将多种数据类型转换成为响应式数据,像原始数据类型、Object、Array、Map、Set 等,我们就以最常用的 Object 进行讲解,学习他们的底层实现方式。

observable 源码梳理

第一步,准备数据结构。

  1. 当我们调用 observable 传入一个原始对象后,Mobx 会先创建一个新的空对象(target)
  2. 基于 target 创建一个响应式对象管理器(ObservableObjectAdministration)
  3. target 会被赋值一个 $mobx 属性并指向 ObservableObjectAdministration
  4. ObservableObjectAdministration 中也会存在一个 target 属性指向 target
  5. 最后基于 target 创建一个 Proxy 对象,并将 Proxy 对象的 get & set 操作都会被代理到 ObservableObjectAdministration 上

image.png

// observable api 在源码中就是下方 createObservable 函数
function createObservable(v) {
    // 在这个函数中会根据传入的数据类型去做对应数据类型的响应式处理 这里我们以 Object 为示例
    if (isObject(v)) {
        return object(v);
    }
}
function object(v) {
    // 1. 创建新的空对象 target 即 asDynamicObservableObject 中传入的 {}
    const observableObject = asDynamicObservableObject({});
    return observableObject;
}
const objectProxyTraps = {
    // 7. 将 Proxy 的 set get 操作代理到 ObservableObjectAdministration
    get(target, name) {
        return getAdm(target).get(name);
    },
    set(target, name, value) {
        return getAdm(target).set(name, value);
    },
};
function getAdm(target) {
    return target[$mobx]
}
function asDynamicObservableObject(target) {
    // 2. 创建 ObservableObjectAdministration
    asObservableObject(target);
    // 6. 基于 target 创建 Proxy
    const proxy = new Proxy(target, objectProxyTraps);
    return proxy;
}
const $mobx = Symbol("mobx administration")
let mobxGuid = 0;
function getNextId() {
    return ++mobxGuid
}
function addHiddenProp(object, propName, value) {
    Object.defineProperty(object, propName, {
        enumerable: false,
        writable: true,
        configurable: true,
        value
    })
}
function asObservableObject(target) {
    const name = `ObservableObject@${getNextId()}`;
    // 3. 基于 target 创建 ObservableObjectAdministration
    const adm = new ObservableObjectAdministration(target, new Map(), name);
    // 5. 向 target 中添加 $mobx 并指向 adm
    addHiddenProp(target, $mobx, adm);
    return target;
}
class ObservableObjectAdministration {
    constructor(target, values, name) {
        // 4. ObservableObjectAdministration 添加 target
        this.target = target;
        this.values = values;
        this.name = name;
    }
    get(key) {
        return this.target[key];
    }
    set(key, value) {
        return (this.target[key] = value);
    }
}

第二步,处理传入的原始数据。

  1. 处理我们在调用 observable 时传入的原始对象,生成响应式数据
  2. 遍历原始对象的每个属性,调用 adm.defineObservableProperty 对每个值进行处理
  3. 先是在 target 上赋值该 key ,值为一个 get 取值函数,最终会在 values 中进行取值
  4. 然后创建 ObservableValue 将真正的值存储到这个响应式对象中
  5. 最后把这个响应式对象存储到 values 这个 Map 里。

image.png

function object(v) {
    const observableObject = asDynamicObservableObject({});
    // 1. 拿到 Proxy 后 调用 extendObservable 处理原始对象的属性
    return extendObservable(observableObject, v);
}
function extendObservable(proxyObject, properties) {
    const descriptors = Object.getOwnPropertyDescriptors(properties)
    // 2. 通过 $mobx 拿到 ObservableObjectAdministration
    const adm = proxyObject[$mobx]
    // 3. 遍历原始对象的所有属性 调用 ObservableObjectAdministration.extend 逐个属性处理
    Reflect.ownKeys(descriptors).forEach(key => {
        adm.extend(key, descriptors[key])
    })
    return proxyObject;
}
class ObservableObjectAdministration {
    constructor(target, values, name) {
        this.target = target;
        this.values = values;
        this.name = name;
    }
    get(key) {
        return this.target[key];
    }
    set(key, value) {
        return (this.target[key] = value);
    }
    extend(key, descriptor) {
        // 4. 调用 defineObservableProperty
        this.defineObservableProperty(key, descriptor.value)
    }
    getObservablePropValue(key) {
        return this.values.get(key).get()
    }
    setObservablePropValue(key, newValue) {
        const observable = this.values.get(key)
        observable.setNewValue(newValue)
        return true;
    }
    defineObservableProperty(key, value) {
        const descriptor = {
            configurable: true,
            enumerable: true,
            get() {
                return this[$mobx].getObservablePropValue(key);
            },
            set(value) {
                return this[$mobx].setObservablePropValue(key, value);
            },
        };
        // 5. 在 target 上定义该属性
        Object.defineProperty(this.target, key, descriptor);
        // 6. 创建 ObservableValue
        const observable = new ObservableValue(value);
        // 8. 将创建的响应式数据存储到 values 上
        this.values.set(key, observable);
    }
}
export class ObservableValue {
    constructor(value) {
        // 7. ObservableValue 才是真正存值的地方
        this.value = value;
    }
    get() {
        return this.value;
    }
    setNewValue(newValue) {
        this.value = newValue
    }
}
  • 最终我们获取某个响应式数据的值时它的取值链条是这样的

image.png

Reaction(副作用函数)

  • 响应式数据创建完成了,那么响应式数据发生变更后,某些函数便需要重新执行,这些就叫做副作用函数。接下来我们看下如何创建副作用函数以及数据变更后执行对应的副作用函数。
  • 在 Mobx 我们可以通过 autorunreactionwhen 等 api 去创建副作用函数,也就是 Reaction 。这里我们以 autorun 这个 api 为示例去学习下这一步的实现逻辑。

autorun 源码梳理

第一步,依赖收集

  • 想要实现数据变更便执行对应的副作用函数,首先要完成依赖收集
  • Reaction 在第一次执行过程中会收集对应的 ObservableValue

image.png

  • Reaction 执行完毕后 再由 ObservableValue 去收集对应的 Reaction

image.png

  • 这个依赖的收集是双向收集的
// Reaction
const globalState = {
    pendingReactions: [],
    trackingDerivation: null,
};
function autorun(view) {
    const name = "Autorun@" + getNextId();
    // 1. 创建对应的 reaction 副作用实例
    const reaction = new Reaction(
        name,
        function () {
           // 6. 收集依赖
           this.track(view)
        }
    )
    // 2. autoRun 会初始便执行一次 调用 schedule 执行 reaction
    reaction.schedule()
}
class Reaction {
    constructor(name = "Reaction@" + getNextId(), onInvalidate) {
        this.name = name;
        this.onInvalidate = onInvalidate;
        this.observing = [];
    }
    track(fn) {
        // 7. 在执行 reaction 前将 trackingDerivation 赋值为自己
        globalState.trackingDerivation = this
        // 8. 执行 fn 时会访问 响应式数据 在这里会调用 ObservableValue 的 get 方法
        fn.call();
        // 13. 副作用函数执行完毕后 重置 trackingDerivation
        globalState.trackingDerivation = null;
        // 14. 完成依赖收集
        bindDependencies(this)
    }
    schedule() {
        // 3. 将要执行的 reaction 添加到 pendingReactions 队列中
        globalState.pendingReactions.push(this)
        runReactions()
    }
    runReaction() {
        // 5. 执行 new Reaction 时传入的 fn
        this.onInvalidate();
    }
}
function bindDependencies(derivation) {
    const { observing } = derivation;
    // 15. 将当前 reaction 保存到对应的 响应式数据的 observers 中
    observing.forEach(observable => {
        observable.observers.add(derivation)
    });
}
function runReactions() {
    // 4. 执行 pendingReactions 中所有的 reaction
    const allReactions = globalState.pendingReactions
    let reaction;
    while (reaction = allReactions.shift()) {
        reaction.runReaction()
    }
}
// ObservableValue
class ObservableValue {
    constructor(value) {
        this.value = value;
        this.observers = new Set();
    }
    get() {
        // 9. 通知观察者收集 ObservableValue
        reportObserved(this)
        // 12. 取值完成
        return this.value;
    }
}
function reportObserved(observable) {
    // 10. 通过 trackingDerivation 取到此时正在执行的 reaction
    const derivation = globalState.trackingDerivation
    if (derivation !== null) {
        // 11. 将当前的 响应式数据 保存到 reaction 中的 observing 中
        derivation.observing.push(observable);
    }
}

第二步,通知更新

  • 依赖收集完成后, 修改响应式数据时如何通知对应的 Reaction 执行就简单了
  • 在 ObservableValue 的 observers 拿到对应的 Reaction 执行它的 runReaction 即可
// ObservableValue
class ObservableValue {
    constructor(value) {
        this.value = value;
        this.observers = new Set();
    }
    get() {
        reportObserved(this)
        return this.value;
    }
    setNewValue(newValue) {
        // 1. 修改响应式数据 最终会触发这里的 set 方法
        this.value = newValue;
        // 2. 广播响应式数据发生了修改
        propagateChanged(this)
    }
}
function propagateChanged(observable) {
    const observers = observable.observers;
    // 3. 通过收集的 observers 拿到所有使用到该数据的 Reaction
    observers.forEach(observer => {
        // 4. 通知 Reaction 执行
        observer.onBecomeStale()
    })
}
// Reaction
class Reaction {
    constructor(name = "Reaction@" + getNextId(), onInvalidate) {
        this.name = name;
        this.onInvalidate = onInvalidate;
        this.observing = [];
    }
    schedule() {
        // 6. 将要执行的 Reaction 放入队列
        globalState.pendingReactions.push(this)
        runReactions()
    }
    runReaction() {
        // 8. 执行传入的 fn 就是 autorun 传入的函数
        this.onInvalidate();
    }
    onBecomeStale() {
        // 5. 执行 Reaction
        this.schedule()
    }
}
function runReactions() {
    // 7. 清空队列 依次执行
    const allReactions = globalState.pendingReactions
    let reaction;
    while (reaction = allReactions.shift()) {
        reaction.runReaction()
    }
}

Actions & Derived values(动作以及派生状态)

  • 由于本文主要探究的是 Mobx 响应式的核心实现,Action 及 Derived Values 并不属于响应式核心,所以只大致介绍下其在 Mobx 中起到的作用,具体如下。
    • Action 称之为动作,Mobx 规定修改响应式数据必须通过 Action 来修改。Action 实际上在 Mobx 中的作用为通过 startBatch、endBatch 事务封装执行动作,将多次响应式状态变更复合为一,等多次状态变更完成后才会执行后续 Reaction ,避免了多次状态变更导致多次副作用函数无意义执行,优化了性能。 image.png
    • Derived values 称为派生状态,当 State 数据发生变更后会通过某些计算方式由变更后的 State 派生出新数据,进而再去执行 Reactions 副作用函数,这也就是我们都熟悉的 Computed values 。本文并未对此部分做过多阐述,只重点关注了当 State 变更后直接就需要自动运行的 Reactions(副作用)即下图中的第二部分。 image.png

总结

  • 以上就是这篇文章的全部内容了,希望通过这篇文章可以帮助大家对 Mobx 的响应式做一个入门,让大家清楚在 Mobx 里是如何创建响应式对象的,如何进行依赖收集的,数据变更后如何执行副作用函数的。
  • 其实这篇文章提到的内容仅仅是 Mobx 里的冰山一角,感兴趣的同学可以借此机会深入了解下 Mobx 的源码, 学习下其他数据结构的处理,Mobx 的事务的处理等等,它还有很多矿藏等你去发掘。