Mobx 源码解析 一(observable)

5,868 阅读7分钟

招聘广告

各位英才,简历可直接发送至 ivanxjfan@tencent.com

请简历名称提供格式:姓名-职位-工作年限-工作地点(如:张三-前端开发-五年-长沙.pdf)


公司:腾讯

地点:长沙

职位:web前端开发工程师

岗位职责:

负责腾讯云DNSPod产品的系统研发工作,完成系统前端功能及后端逻辑代码实现,保障产品质量及研发进度。

岗位要求:

1、本科以上学历,计算机相关专业,2年以上工作经验;

2、精通Javascript、html、css等前端开发技术,基础扎实;

3、熟悉当下主流的前端框架(react/vue等),有react、redux开发经验优先;

4、熟悉HTTP、TCP/IP协议;拥有良好的安全意识,熟悉常见的网络安全攻防策略;

5、具有良好的分析问题、解决问题能力和学习热情;

6、有Node.js/PHP开发经验优先;

7、有过WP或DZ的插件开发者优先

注:此岗位为腾讯集团旗下全资子公司编制 岗位”


公司:腾讯

地点:深圳腾讯总部

职位: 高级 web 前端开发工程师

岗位职责:

负责腾讯云域名产品( DNSPod )的系统架构设计和研发

岗位要求:

1、本科以上学历,计算机相关专业,至少 5 年以上工作经验;

2、精通 Javascript 、html 、css 等前端开发技术,基础扎实;

3、熟悉当下主流的前端框架(react/vue 等),有 react 、redux 开发经验优先;

4、熟悉 HTTP 、TCP/IP 协议;拥有良好的安全意识,熟悉常见的网络安全攻防策略;

5、具有良好的分析问题、解决问题能力和学习热情;

6、有 Node.js/PHP 开发经验优先;

前言

在Git 找到Mobx 的源码, 发现其是使用TypeScript 编写,因为我对Typescrit 没有项目经验,所以我先会将其编译成JavaScript,所以我们可以运行如下脚本或者从CDN直接下载一份编译过的源码,我们可以选择umd 规范脚本:

  1. git clone git@github.com:mobxjs/mobx.git
  2. npm i
  3. npm run quick-build

我直接从CDN 下载了一份源码, 然后进行分析。

Demo

首先我们从一个最基本的Demo开始,来看Mobx 的基本使用方式:

const addBtn = document.getElementById('add')
const minusBtn = document.getElementById('minus')
const incomeLabel = document.getElementById('incomeLabel')
const bankUser = mobx.observable({
    name: 'Ivan Fan',
    income: 3,
    debit: 2
});

const incomeDisposer = mobx.autorun(() => {
    incomeLabel.innerText = `Ivan Fan income is ${bankUser.income}`
})

addBtn.addEventListener('click', ()=> {
    bankUser.income ++
})
minusBtn.addEventListener('click', () => {
    bankUser.income --
})

我们的界面非常简单,如图:

图1 两个Button , 一个label. 我们在js 文件中,我们给两个按钮添加了**click** 事件,事件的主体非常简单`bankUser.income ++` `bankUser.income --`, 就是对`bankuser` 的`income` 属性进行了自增或者自减,非常神奇, 当我们点击对应的按钮的时候, 中间的label 的内容发生了变化。但是我们在Button 的点击事件中并没有去操作**incomeLabel** 的内容,但是其内容确实随着点击事件,实时发生了变化。究其原因,只有以下代码对**incomeLabel** 的text 进行了处理: ``` const incomeDisposer = mobx.autorun(() => { incomeLabel.innerText = `Ivan Fan income is ${bankUser.income}` }) ``` 这就是**Mobx** 的最简单神秘的功能,我们可以先从此开始深入研究它。

observable

从上面的JS文件中,我们发现其中引用了mobx 两个方法,分别是observableautorun,是的,是这样两个方法,让incomeLabel 在点击按钮的时候实时的发生了变化,所以我们接下来会对这两个方法进行深入分析,这一章节我们会先分析observable 先进行分析。 我们先打开Mobx的源码, 如果我们用Vscode 打开这个源码,我们可以用快捷键Ctrl + K Ctrl + 0 将代码都折叠起来, 然后在打开, 找到exports 的代码块,我们可以查看mobx 都暴露出了哪些方法:

图2 暴露了一些列方法,我们后续会使用。

observable,翻译成中文就是可以观测的, 我们现在来调试这个方法, 我们可以const bankUser = mobx.observable({ 这一行打一个断点,然后F11,跳进去,发现源码对应的是一个createObservable 方法,也就是创建一个可以观察的对象:

var observable$$1 = createObservable;
function createObservable(v, arg2, arg3) {
    if (typeof arguments[1] === "string") {
        return deepDecorator$$1.apply(null, arguments);
    }
    if (isObservable$$1(v))
        return v;
    var res = isPlainObject$$1(v)
        ? observable$$1.object(v, arg2, arg3)
        : Array.isArray(v)
            ? observable$$1.array(v, arg2)
            : isES6Map$$1(v)
                ? observable$$1.map(v, arg2)
                : v;
    if (res !== v)
        return res;
    // otherwise, just box it
    fail$$1(process.env.NODE_ENV !== "production" &&
        "The provided value could not be converted into an observable. If you want just create an observable reference to the object use 'observable.box(value)'");
}

上面代码很简单,参数有三个,但是我们在调用的时候,值传递了一个参数, 所以我们暂且只要关心第一个参数 r .以下是这个functin 的基本逻辑:

  1. 如果传入的第二个参数是一个字符串, 则直接调用deepDecorator?1.apply(null, arguments);
  2. 判断第一个参数是否已经是一个可观察的对象了,如果已经是可观察的对象了,就直接返回这个对象
  3. 判断第一个参数是什么类型,然后调用不同的方法, 总共有三种类型: object , array , map (ES 的Map 数据类型), 分别调用:observable?1.object, observable?1.array, observable?1.map方法, 那这个observable?1又是什么呢?在第一行var observable?1 = createObservable;表面就是createObservable方法。但是这个方法就短短几行代码,并没有object, array, map着三个方法, 我们发现在这个方法下面有observableFactories 对象,其是一个工厂对象,用来给createObservable添加方法,其定义了这三个方法,并且通遍历过Object.keys(observableFactories).forEach(function (name) { return (observable?1[name] = observableFactories[name]); });

因为在我们的Demo 中我们传递的是一个Object, 所以会调用observable?1.object 方法,接下来我们在继续分析这个方法, 其代码如下:

    object: function (props, decorators, options) {
        if (typeof arguments[1] === "string")
            incorrectlyUsedAsDecorator("object");
        var o = asCreateObservableOptions$$1(options);
        if (o.proxy === false) {
            return extendObservable$$1({}, props, decorators, o);
        }
        else {
            var defaultDecorator = getDefaultDecoratorFromObjectOptions$$1(o);
            var base = extendObservable$$1({}, undefined, undefined, o);
            var proxy = createDynamicObservableObject$$1(base);
            extendObservableObjectWithProperties$$1(proxy, props, decorators, defaultDecorator);
            return proxy;
        }
    },

var o = asCreateObservableOptions?1(options); 生成的了一个简单的对象:

var defaultCreateObservableOptions$$1 = {
    deep: true,
    name: undefined,
    defaultDecorator: undefined,
    proxy: true
};

o.proxy 的值为true, 所以会走else 逻辑分支, 所以接下来我们一一分析else 分支中的每一条代码。

  1. var defaultDecorator = getDefaultDecoratorFromObjectOptions?1(o); 这个是跟装饰器有关的逻辑,我们先跳过
  2. var base = extendObservable?1({}, undefined, undefined, o); 对o对象进行了加工处理,变成了一个Symbol 数据类型。

这一步操作非常重要,给一个空对象添加了一个$mobx?1(var $mobx?1 = Symbol("mobx administration");)的属性, 其值是一个 ObservableObjectAdministration 类型对象,其write 方法在后续数据拦截中会调用。

图3
  1. var proxy = createDynamicObservableObject?1(base); 这个方法,最为核心, 对这个对象进行了代理(Proxy)

图4

对这个对象的属性的get, set, has, deleteProperty, ownKeys, preventExtensions方法进行了代理拦截,这个是Mobx 事件数据添加的一个核心点。

  1. 第三点的proxy 其实只是初始化了一个简单的代理对象,但是没有与我们需要观察的target(也就是mobx.observable方法传递进来的需要被观察的对象)关联起来, extendObservableObjectWithProperties?1(proxy, props, decorators, defaultDecorator); 方法会遍历target 的属性,将其赋值给proxy对象, 然后我们mobx.observable 里的对象都被代理了,也就是实现了对属性操作的拦截处理。

  2. 在第四点extendObservableObjectWithProperties?1 方法中, 最终会给原始的对象的属性进行装饰,通过查看function 的 call stack 得知,最后对调用ObservableObjectAdministration 的addObservableProp 方法, 针对每一个propName(原始对象的Key)生成一个ObservableValue 对象,并且保存在ObservableObjectAdministration 对象的values

图三中发现, 真正实现数据拦截的就是objectProxyTraps 拦截器, 下一章节,我们需要对这个拦截器进行深入分析,着重看get,set如何实现了数据拦截。

  1. return proxy; 最终将返回一个已经被代理过的对象,替换原生对象。

bankUser 对象就是一个已经被代理了的对象,并且包含了一个Symbol 类型的新的属性。

const bankUser = mobx.observable({
    name: 'Ivan Fan',
    income: 3,
    debit: 2
});

总结

  1. observable 首先传入一个原始对象(可以传入多种类型的数据: array, map, object, 现在只分析object 类型的情况)
  2. 创建一个空的Object 对象,并且添加一些默认属性(var base = extendObservable?1({}, undefined, undefined, o);), 包括一个Symbol类型的属性,其值是一个ObservableObjectAdministration 类型的对象.
  3. 将这个对象用ES6 的Proxy 进行了代理, 会拦截这个对象的一些列操作(get, set...) var proxy = new Proxy(base, objectProxyTraps);
  4. 将原始对象,进行遍历,将其所有的自己的属性挂载在新创建的空对象中
  5. 返回已经加工处理的对象bankUser
  6. 后续就可以监听这个对象的相应的操作了。
  7. 加工后的对象如下图所示, 后面操作的对象,就是如下这个对象,但是observable 方法,其实只是做到了如下图的第二步(2), 第三步(3)的observers属性还是一个没有任何值的Set 对象,在后续分析autorun 方法中,会涉及到在什么时候去给它赋值