mobx的简单解析

前言

最近总爱翻这种巧而精的库的源码,一个是代码量小,容易调试懂,另一个是实现思路巧,能学到不少东西。那么今天,我们翻谁的源码呢?如题,那就是mobx的。

mobx作为一个状态管理库,小巧而轻便,门槛低,官方文档更是没几个字,api数量不多,去掉一些怎么用到的when、flow这些,更是少了。而且它不止学习成本低,它使用起来也非常简单。正因为这些,它广受开发者们的喜爱。

不过,今天我们并不是来学习它如何使用的,而是学习它都要做什么以及如何做的。所以下文不会有mobx的入门相关的内容,我们今天是来讲它的原理的。

实现一个简单版的mobx

依我看来,只要掌握了autorun和observable这个两个api,基本就是了解整个mobx的实现思路了,从而实现它的其他api都是易如反掌,所以下面我们就来实现一个这两个api。

先看个简单例子:

js 复制代码
import { observable, autorun } from 'mobx';

let obj = { name: 1 };
// observable可以把一个普通对象变成可观察对象
let proxyObj = observable(obj);
// 通过autorun可以创建一个响应,类似useEffect,创建时执行一次,里面的值改变时也会自动执行
autorun(() => {
  console.log(proxyObj.name);
})
proxyObj.name = 2; // 此时autorun会执行,打印2
proxyObj.name = 3; // 此时autorun会执行,打印3

如代码中的注释,autorun在创建时会执行一次,后面如果改变了autorun里用到的可观察对象,那么autorun都会再次执行,就像代码中连续两次修改proxyObj.name一样,autorun就跟着自动执行了两次。

看到这里,其实不难看出,mobx就是在所用到的可观察对象的set上做了手脚,当改变了可观察对象的值(即是调用了set),那么就执行一下相关的autorun。

不过在讲这个autorun的实现之前,我们要先知道可观察对象也就是observable都做了什么。

observable

observable其实就是双向绑定的实现,不过mobx有意思的地方在于,它给每一个需要双向绑定的(或者说成为可观察对象)的数据都安排了一个管家,我们想要访问或者改写这个可观察对象,都是要通过这个管家的。

实现过双向绑定的应该知道,不同数据类型的双向绑定的实现方式其实稍有不同,所以本文不讲解observable中对所有数据类型的实现,只讲对Object的实现。

那么我们就写下这样一段代码:

js 复制代码
import { object } from './observableobject';

function isObject(value) {
    return value !== null && typeof value === 'object';
}

function observable(v) {
    if (isObject) {
        return object(v);
    }
}

export default observable;

从代码可以看出,observable的逻辑都在object这个方法中。

前排警告:以下代码逻辑非常绕

object这个方法涉及到的逻辑非常复杂,这边先看一下文字版逻辑顺序,再直接看代码,相信各位都能看懂。

以以下这段代码为例

js 复制代码
let obj = { name: 1 };
// observable可以把一个普通对象变成可观察对象
let proxyObj = observable(obj);
  • 1、创建一个空对象{}(下文都称之为obj2),并将他转成一个Proxy,并且设定一个独特的捕获器(下文会讲这个捕获器)
  • 2、生成一个管家,管家类(类名为ObservableObjectAdministration)的一个实例(即是 new ObservableObjectAdministration),下文都称这个实例为adm。并给这个管家生成一个独有的名字,这个名字的生成规则为ObservableObject@ + 一个自增的id,保证了唯一性。
  • 3、给obj2增加一个不可改写的属性,属性名为Symbol('mobx administration'),这个属性名我们称之为$mobx,属性值为adm。
  • 4、遍历obj(即是代码中的{ name: 1 }这个对象)的自有属性,都执行一次adm的extend方法。

到这先暂停,我们来看一下这个管家类。

管家类的所有代码都附上来了,但我们先把目光放在extend和defineObservableProperty上。

js 复制代码
class ObservableObjectAdministration {
    // target就是那个obj2
    // values是一个new Map()
    // name是管家的名字,即ObservableObject@ + 一个自增的id
    constructor(target, values, name) {
        this.target = target;
        this.values = values; // 存放属性的信息
        this.name = name;
    }

    get(key) {
        return this.target[key];
    }
    set(key, value) {
        if (this.values.has(key)) {
            return this.setObservableValue(key, value);
        }
    }
    extend(key, descriptor) {
        this.defineObservableProperty(key, descriptor.value);
    }
    getObservableValue(key) {
        return this.values.get(key).get();
    }
    setObservableValue(key, value) {
        const observableValue = this.values.get(key);
        observableValue.setNewValue(value);
        return true;
    }
    defineObservableProperty(key, value) {
        const descriptor = {
            configurable: true,
            enumerable: true,
            get() {
                // 注意,这里的this指向的是obj2
                return this[$mobx].getObservableValue(key);
            },
            set() {
                // 注意,这里的this指向的是obj2
                return this[$mobx].setObservableValue(key, value);
            }
        }
        Object.defineProperty(this.target, key, descriptor);
        this.values.set(key, new ObservableValue(value));
    }
}

看完上述代码,我们可以看出,extend方法其实就是将obj上面的属性都赋值给adm的target和values,并且会将值做一次加工,就是new ObservableValue(value)这里。

注意:由于复杂类型的引用原因,其实这里的target和obj2是一样的,给target赋值,其实也就是给obj2赋值了。

这里贴一下ObservableValue这个类的代码,先不用关心里面都做了什么。

js 复制代码
class ObservableValue {
    constructor(value) {
        this.value = value;
        this.observers = new Set(); // 此可观察值的监听者,或者说是观察者
    }
    get() {
        reportObserved(this);
        return this.value;
    }
    setNewValue(newValue) {
        this.value = newValue;
        propagateChanged(this);
    }
}

好!铺垫到这里,还记得上文第一步生成obj2时谈到的捕获器吗?这里我们就来聊聊他。

obj2生成转成Proxy可以看成以下代码。

js 复制代码
// 就是获取到obj2身上的adm
function getAdm(target) {
    return target[$mobx];
}

const objectProxyTraps = {
  // get方法改写成adm的get,也就是从adm上的values读取值
  get(target, name) {
    return getAdm(target).get(name);
  },
  // 同理,set方法改写成adm的set,也就是执行adm的set方法
  set(target, name, value) {
    return getAdm(target).set(name, value);
  }
};

const obj2 = new Proxy({}, objectProxyTraps);

其实上面这个转成Proxy的代码,一句话就能说清楚,就是当从obj2身上读和写属性时,都改成走adm的对应方法。

那么好,整个observable的最后一步做什么呢?

  • 5、返回obj2。

对的,就是这么简单,把obj2返回回去。

那么经过了这么一大堆的逻辑处理,现在都成什么样子了呢?

举个例子,假设我们想要读取obj的name属性,会发生以下这样的步骤:

读取obj.name -> 执行obj2.get() -> 从obj2上获取到adm -> 执行adm.get(name) -> 读取到adm.target.get(name)

总的来说,交给observable的数据,他们都成了少爷,不但被分配了一个专属管家,而且任何事情都会交给管家做。

autorun

上面的observable的逻辑是非常绕的,如果看不太懂也没关系,不会很影响看懂autorun。

autorun的设计思想是很好理解的。

我们在定义autorun时,autorun会执行一次,而当执行到可观察对象的时候,必定会执行可观察对象的get方法(也就是读取属性值)。那么我们就可以在这个get方法上做一下文章,通过这个get方法让这个属性绑定上这个autorun。

而当我们改变绑定过autorun的属性的值的时候,我们就让绑定的autorun们都执行一遍即可。

主要思路我们清楚了,那么下面来看看实现步骤:

  • 1、定义一个全局变量globalState
  • 2、定义autorun时,生成一个独有的autorun的名字并且生成一个叫Reaction(下文有完整代码)的类的实例(下文成reaction),将reaction赋值给globalState.trackingDerivation
  • 3、执行定义autorun时传入的函数
  • 4、每当执行可观察对象的get方法时,就将自身push到globalState.trackingDerivation(即是reaction)的observing数组中
  • 5、函数执行完毕后,将globalState.trackingDerivation置为null
  • 6、遍历observing,将自身(即是reaction)都push进他们的observers数组中。

上面这些步骤就是一个双向绑定的过程。可观察对象自身有个observers数组,而reaction自身有个observing数组,通过全局变量globalState桥接,互相将自己都push进了各自的数组中,实现了双向绑定。

有了这一个双向绑定之后,只要我们改了可观察对象的值的时候,依次执行一下observers里的reaction就好了。

完整代码

  • observable.js
js 复制代码
import { object } from './observableobject';
import { isObject } from './utils';

function observable(v) {
    if (isObject) {
        return object(v);
    }
}

export default observable;
  • observableobject.js
js 复制代码
import { getNextId, addHiddenProp, $mobx, getAdm, globalState } from './utils';

class ObservableValue {
    constructor(value) {
        this.value = value;
        this.observers = new Set(); // 此可观察值的监听者,或者说是观察者
    }
    get() {
        reportObserved(this);
        return this.value;
    }
    setNewValue(newValue) {
        this.value = newValue;
        propagateChanged(this);
    }
}

function propagateChanged(observableValue) {
    const { observers } = observableValue;
    observers.forEach(observer => {
        observer.runReaction();
    })
}

function reportObserved(observableValue) {
    const trackingDerivation = globalState.trackingDerivation;
    if (trackingDerivation) {
        trackingDerivation.observing.push(observableValue);
    }
}

class ObservableObjectAdministration {
    constructor(target, values, name) {
        this.target = target;
        this.values = values; // 存放属性的信息
        this.name = name;
    }

    get(key) {
        return this.target[key];
    }
    set(key, value) {
        if (this.values.has(key)) {
            return this.setObservableValue(key, value);
        }
    }
    extend(key, descriptor) {
        this.defineObservableProperty(key, descriptor.value);
    }
    getObservableValue(key) {
        return this.values.get(key).get();
    }
    setObservableValue(key, value) {
        const observableValue = this.values.get(key);
        observableValue.setNewValue(value);
        return true;
    }
    defineObservableProperty(key, value) {
        const descriptor = {
            configurable: true,
            enumerable: true,
            get() {
                return this[$mobx].getObservableValue(key);
            },
            set() {
                return this[$mobx].setObservableValue(key, value);
            }
        }
        Object.defineProperty(this.target, key, descriptor);
        this.values.set(key, new ObservableValue(value));
    }
}

function asObservableObject(target) {
    const name = `ObservableObject@${getNextId()}`;
    const adm = new ObservableObjectAdministration(
        target, new Map(), name,
    );
    addHiddenProp(target, $mobx, adm);
    return target;
}

const objectProxyTraps = {
    get(target, name) {
        return getAdm(target).get(name);
    },
    set(target, name, value) {
        return getAdm(target).set(name, value);
    }
};

function asDynamicObservableObject(target) {
    asObservableObject(target);
    const proxy = new Proxy(target, objectProxyTraps);
    return proxy;
}

function extendObservable(proxyObject, properties) {
    const descriptors = Object.getOwnPropertyDescriptors(properties);
    const adm = proxyObject[$mobx];
    Reflect.ownKeys(descriptors).forEach(key => {
        adm.extend(key, descriptors[key]);
    })
    return proxyObject;
}

export function object(target) {
    const dynamicObservableObject = asDynamicObservableObject({});
    return extendObservable(dynamicObservableObject, target);
}
  • autorun.js
js 复制代码
import { getNextId } from "./utils";
import Reaction from './reaction';

function autorun(view) {
    const name = 'Autorun@' + getNextId();
    const reaction = new Reaction(
        name,
        function () {
            this.track(view);
        }
    );
    reaction.schedule();
}

export default autorun;
  • reaction
js 复制代码
import { globalState } from "./utils";

export default class Reaction {
    constructor(name, onInvalidate) {
        this.name = name;
        this.onInvalidate = onInvalidate;
        this.observing = []; // 表示观察到了哪些可观察变量
    }
    track(fn) {
        globalState.trackingDerivation = this;
        fn.call();
        globalState.trackingDerivation = null;
        bindDependencies(this);
    }
    schedule() {
        globalState.pendingReaction.push(this);
        runReactions();
    }
    runReaction() {
        this.onInvalidate();
    }
}

function bindDependencies(derivation) {
    const { observing } = derivation;
    observing.forEach((observableValue) => {
        observableValue.observers.add(derivation);
    });
}

function runReactions() {
    const allReactions = globalState.pendingReaction;
    let reaction;
    while (reaction = allReactions.shift()) {
        reaction.runReaction();
    }
}
  • utils.js
js 复制代码
export const $mobx = Symbol('mobx administration');

export function isObject(value) {
    return value !== null && typeof value === 'object';
}
let mobxGuid = 0;
export function getNextId() {
    return ++mobxGuid;
}

export function addHiddenProp(obj, propName, value) {
    Object.defineProperty(obj, propName, {
        enumerable: false,
        writable: true,
        configurable: false,
        value,
    });
}

export function getAdm(target) {
    return target[$mobx];
}

export const globalState = {
    pendingReaction: [],
    trackingDerivation: null,
}

mobx-react

既然都理解了mobx的原理了,那么这里顺带提一嘴mobx-react的一些原理。

在我们使用mobx-react的时候,他的效果是什么?

很简单,就是当可观察对象的值变的时候,组件自己自动重新渲染一下。注意是自动,这个自动二字有没非常熟悉?对了!那就是autorun。我们只要将setState({})传给autorun就能自己实现一个mobx-react的效果了。

不过,实际上,mobx-react是直接用Reaction来做的。这个Reaction和autorun里用到的Reaction就是同一个。

这里就实现一个useObserver好了。

js 复制代码
import React, { useState } from 'react';
import { Reaction } from 'mobx';

export function useObserver(fn) {
    // 仅仅是为了得到一个强行更新组件的函数
    const [, setState] = useState({});
    const forceUpdate = () => setState({});
    let reaction = new Reaction('observer', forceUpdate);
    let rendering;
    reaction.track(() => {
        rendering = fn();
    }, forceUpdate);
    return rendering;
}

结尾

本文主要讲了mobx的observable和autorun的原理和实现,然后通过autorun引出了mobx-react的useObserver的原理和实现。

读懂了本文,不但可以收获到mobx和mobx-react的基本原理,还能明白双向绑定的原理和应用,而且大量的类的使用,对于写工具库也是非常有帮助的。

以下附上代码仓库,仓库中的代码不仅只实现了mobx-react的useObserver,还有另外几个api,感兴趣的可以拉下来看看。

simple-mobx

最后:创作不易,给个小赞啦~

相关推荐
也无晴也无风雨1 小时前
深入剖析输入URL按下回车,浏览器做了什么
前端·后端·计算机网络
Martin -Tang2 小时前
Vue 3 中,ref 和 reactive的区别
前端·javascript·vue.js
FakeOccupational3 小时前
nodejs 020: React语法规则 props和state
前端·javascript·react.js
小牛itbull3 小时前
ReactPress:构建高效、灵活、可扩展的开源发布平台
react.js·开源·reactpress
放逐者-保持本心,方可放逐3 小时前
react 组件应用
开发语言·前端·javascript·react.js·前端框架
曹天骄4 小时前
next中服务端组件共享接口数据
前端·javascript·react.js
阮少年、5 小时前
java后台生成模拟聊天截图并返回给前端
java·开发语言·前端
郝晨妤6 小时前
鸿蒙ArkTS和TS有什么区别?
前端·javascript·typescript·鸿蒙
AvatarGiser6 小时前
《ElementPlus 与 ElementUI 差异集合》Icon 图标 More 差异说明
前端·vue.js·elementui
喝旺仔la6 小时前
vue的样式知识点
前端·javascript·vue.js