【用故事解读 MobX 源码(五)】 Observable

================媒介===================

=======================================

A. Story Time

最高警长看完实行官(MobX)的自动布置计划,对 “视察员” 这个下层职员事情比较感兴趣,自实行官拿给他布置计划的时刻,他就注重到统统上层职员的功用都是基于该底层职员高效的事情机制;

第二天,他找上实行官(MobX)一同去视察“视察员”地点机构部门(下面简称为 ”视察局“),想更深切地相识 “视察员” 运转分派机制。

《【用故事解读 MobX 源码(五)】 Observable》

当最高警长抵达部门的时刻,正好碰到该部门正好要最先实行 MobX 前不久新下发的任务,请求监控 parent 对象的一举一动:

var parent = {
  child: {
    name: 'tony',
    age: 15
  }
  name: 'john'
}

var bankUser = observable(parent);

任务到达视察局办公室后,相应的办公室文员会对任务举行剖析,然后会依据对象范例交给相应科室举行处置惩罚,罕见的有 object 科,别的另有 map 科和 array 科;

如今,办公室文员见传入的对象是 parent 是个对象,就将其通报给 object 科,让其组织起一同针对该 parent 对象的 ”视察小组“,组名为 bankUser

object 科接到任务,委派某位科长(以下称为 bankUser 科长)组成专项担负此 parent 对象的视察事情,bankUser 科长接办任务后发明有两个属性,个中 child 是对象范例,age 是原始值范例,就离别将任务委派给 child 小科长 和 name 视察员 O1,child 小科长接到任务后再委派给 name 视察员 O2 和 age 视察员 O3,终究实行该任务的职员组织以下:

《【用故事解读 MobX 源码(五)】 Observable》

视察员的任务职责我们已很熟习了,当读写视察员对应的数据时将触发 reportObservedpropagateChanged 要领;

这里涉及到两位科长(bankUser 科长 和 child 小科长),那末科长的任务职责是什么呢?

科长的人物职责是起到 治理 作用,它担负统管在他名下的视察员。比方当我们读写 bankUser.child 对象的 name 属性时(比方实行语句 bankUser.child.name = 'Jack'),起首感知到读写支配的并非是 视察员 O2 而是bankUser科长bankUser科长会示知 child 小科长有数据变动,child 小科长然后再将信息转达给 name 视察员 O2 ,然后才是视察员 O2 对数据读写起回响反映,这才让视察员 O2 发挥作用。

《【用故事解读 MobX 源码(五)】 Observable》

从代码层面看,我们看到仅仅是实行 bankUser.child.name = 'Jack'这一行语句,和我们寻常修正对象属性并没有二致。但是在这一行代码背地实在牵动了一系列的支配。这实际上是 MobX 构建起的一套 ”镜像“ 系统,运用者依旧按日常平凡的体式格局读写对象,但是每一个属性的读写支配实则都镜像到视察局 的某个小组仔细的支配;异常类似于古代的 ”垂帘听政“ ,看似天子坐在文武百官前面,实在真正做出决议计划相应的是藏在帘背面的那个人。

前几章中我们只看到视察员在运动,但是背地离不开 科长 这一角色机制在背地悄悄的调理。对每项任务,终究都邑落实到视察员采纳“一对一”形式监控分派到给自身的视察项,而每一个视察员肯定是隶属于某个 ”科长“ 率领。在 MobX 系统里,办公室、科长和视察员是密不可分,配合构建起 视察局 运转系统体例;

“分工邃晓,运转高效”,这是最高警长在巡查完视察员培训基地后的第一印象,视察局运转的每一步的设想都有邃密的考量;

B. Source Code Time

先排列本文故事中人物与 MobX 源码观点映照关联:

故事人物MobX 源码诠释
警署最高主座(无)MobX 用户,没错,就是你
实行官 MobXMobX全部 MobX 运转环境
视察局办公室(主任、文员) observableobservable.box用于竖立 Observable 的 API
object 科室、map 科室、array 科室 observable.objectobservable.mapobservable.array将差异复合范例转换成视察值的要领
科长ObservableObjectAdministration主要给对象增加 $mobx 属性
视察员 ObservableValue 实例ObservableValue 实例

1、总进口:observable

observable 对应上述故事中的 视察局办公室主任 角色,自身不供应转换功用,主假如起到一致调理作用 —— 如许 MobX 实行官只须要将敕令发给办公室职员就行,至于内部仔细的支配、仔细由哪一个科室处置惩罚,MobX 实行官不须要体贴。

将与 observable 的源码 相干的源码轻微整顿,就是以下的情势:

var observable = createObservable;
// 运用“新鲜”的体式格局来扩大 observable 函数的功用,就是将 observableFactories 的要领挨个拷贝给 observable
Object.keys(observableFactories).forEach(function(name) {
  return (observable[name] = observableFactories[name]);
});
  • 起首 observable 是函数,函数内容就是 createObservable
  • 其次 observable 是对象,对象属性和 observableFactories 一致

也就是说 observable 实际上是 种种组织器的总和,整合了 createObservable(默许组织器) + observableFactories(其他组织器)

自身也可以在 console 掌握台中打印来考证一番:

const { observable } = mobx;

console.log('observable name:', observable.name);
console.log(Object.getOwnPropertyNames(observable));

从以下掌握台输出的效果来看,observable 的属性确实来自于createObservableobservableFactories 这两者:
《【用故事解读 MobX 源码(五)】 Observable》

笔墨比较死板,用图来示意就是下面那模样:

《【用故事解读 MobX 源码(五)】 Observable》

这里我大抵划分了一下,分红 4 部份内容来邃晓:

  • 第一部份:createObservable 要领适才大略讲过,是 MobX API 的 observable 的别号,是一个高度封装的要领,算是一个总进口,轻易用户挪用;该部份对应上述故事中的 视察局办公室主任 的角色
  • 第二部份:box 是一个转换函数,用于将 原值(primitive value) 直接转换成 ObservableValue 对象;shallowBoxbox 函数的非 deep 版本;该部份对应上述故事中的 视察局办公室文员 的角色;
  • 第三部份:针对 object、array 以及 map 这三种数据范例离别供应转换函数,同时也供应 shallow 的版本;该部份对应上述故事中的 科室 部份;
  • 第四部份:供应四种装潢器函数,装潢器的观点我们上一节课讲过,主要辅佐供应装潢器语法糖作用;对一般 MobX 用户来说这部份日常平凡也是打仗不到的;

怎样邃晓这 4 部份的之前的关联呢?我个人的邃晓以下:

  • 第三部份属于 “下层修建”,离别为 object、array 以及 map 这三种数据范例供应转换成可视察值的功用(默许是递归转换,shallow 示意非递归转换);这部份对应上述故事中的科室观点,差异的视察任务由差异的科室来处置惩罚;
  • 第一部份和第二部份属于 “上层修建”,供应一致的接口,仔细的转换功用都是挪用第三部份中的某个转换函数来完成的;这两部份对应上述故事中的 视察局办公室 部份。
  • 第一部份我们最熟习,不过第二部份的 box 函数转换才能反而比第一部份更广,支撑将原始值转换成可视察值
  • 第四部份和别的三部份没有直接的关联,主要辅佐供应装潢器函数;注重,没有直接的联络并不代表没有联络,第四部份中装潢器内的中心逻辑和别的三部份是一样的(比方都挪用 decorator 要领)。

下面我们看两个仔细的示例,来辅佐消化上面的结论。

示例一observable.box(obj) 底层就是挪用 observable.object(obj)完成的

var user = {
  income: 3,
  name: '张三'
};
var bankUser = observable.object(user);
var bankUser2 = observable.box(user);

console.log(bankUser);
console.log(bankUser2);

《【用故事解读 MobX 源码(五)】 Observable》
可以发明 bankUser2 中的 value 属性部份内容和 bankUser 是如出一辙的。

示例二observable.box(primitive) 能行,observable(primitive) 却会报错

var pr1 = observable.box(2);
console.log(pr1);
console.log('--------华美支解-----------')
var pr2 = observable(2);
console.log(pr2);

从报错信息来看,MobX 会友谊提醒你改用 observable.box 要领完成原始值转换:

《【用故事解读 MobX 源码(五)】 Observable》

2、第一部份:createObservable

正如上面所言,该函数实在就是 MobX API 的 observable 的 “别号”。所以也是对应上述故事中的 视察局办公室主任 角色;

该函数自身不供应转换功用,只是起到 “转发” 作用,将传入的对象转发给对应仔细的转换函数就好了;

看一下 源码

function createObservable(v, arg2, arg3) {
  // 走向 ①
  if (typeof arguments[1] === 'string') {
    return deepDecorator.apply(null, arguments);
  }
  
  // 走向 ②
  if (isObservable(v)) return v;
  
  var res = isPlainObject(v)
    ? observable.object(v, arg2, arg3) // 走向③
    : Array.isArray(v)
      ? observable.array(v, arg2)  // 走向 ④
      : isES6Map(v) ? observable.map(v, arg2) // 走向 ⑤
      : v;
  
  if (res !== v) return res;
  // 走向 ⑥
  fail(
        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)'`
    )
}

不难看出实际上是典范的采纳了 战略设想形式 ,将多种数据范例(Object、Array、Map)状况的转换封装起来,好让挪用者不须要体贴完成细节:

该设想形式参考可参考
深切邃晓JavaScript系列(33):设想形式之战略形式

用图来展现一下仔细的走向:

《【用故事解读 MobX 源码(五)】 Observable》

  • 走向 ① 是 装潢器语法所特有的,这是因为此时传给 createObservable 的第二个参数是 string 范例,这一点我们在上一篇文章有仔细叙述;
  • 走向 ② 很直观,假如传入的参数就已是 视察值 了,不多空话直接返回传入的值,不须要转换;
  • 走向 ③、④ 、⑤ 是直依据传入参数的范例离别挪器具针对仔细范例的转换要领;
  • 走向 ⑥,在上面示例中我们已看到过, 针对原始值会提醒发起用户运用 observable.box 要领。

第一部份的 createObservable 的内容就那末些,总之只是起了 “导游” 作用。是不是是比你设想中的要简朴?

接下来我们继续看第二部份的 observable.box 要领。

3、第二部份:observable.box

这个要领对应上述故事中的 视察局办公室文员 角色,也是属于办公室部门的,所起到的作用和 主任 迥然差异,只是日常平凡我们用得并不多罢了。

当我第一次浏览 官网文档 中针对有关 observable.box 的形貌时:

《【用故事解读 MobX 源码(五)】 Observable》

往返读了频频,“盒子”是个啥?它干吗用的? “observable” 和 “盒子” 有半毛钱关联?

直到看完该函数的仔细引见 boxed values 后,刚刚有所感悟,这里这 box 要领就是将一般函数 “包装” 成可视察值,所以 box 是动词而非名词

正确地邃晓,observable.box 是一个转换函数,比方我们将一般的原始值 “Pekin”(北京)转换成可视察值,就可以运用:

const cityName = observable.box("Pekin");

原始值 “Pekin” 并不具有可视察属性,而经由 box 要领支配以后的 cityName 变量具有可视察性,比方:

console.log(cityName.get());
// 输出 'Pekin'

cityName.observe(function(change) {
    console.log(change.oldValue, "->", change.newValue);
});

cityName.set("Shanghai");
// 输出 'Pekin -> Shanghai'

从输入输出角度来看,这 box 实在就是将一般对象转换成可视察值的历程,转换历程当中将一系列才能“增加”到对象上,从而取得 “自动相应数值变化” 的才能。

那末仔细这 box 函数是怎样完成的呢?直接看 源码

box: function(value, options) {
  if (arguments.length > 2) incorrectlyUsedAsDecorator('box');
  var o = asCreateObservableOptions(options);
  return new ObservableValue(
    value,
    getEnhancerFromOptions(o),
    o.name
  );
}

发明该要领仅仅是挪用 ObservableValue 组织函数,所以 box 要领支配的效果是返回 ObservableValue 实例。

这里的
asCreateObservableOptions 要领仅仅是花样化入参
options 对象罢了。

4、中心类:ObservableValue

总算是讲到这个 ObservableValue 类了,该类是邃晓可视察值的症结观点。这个类对应上述故事中的 视察员 角色,就是最下层的 name 视察员 O1、O2、O3 那些。

本篇文章的终究目的也就是为了讲清楚这个 ObservableValue 类,其他的观点反而是缭绕它而竖立起来的。

剖析其源码,将这个类的属性和要领都拎出来瞧瞧,绘制成类图大抵以下:

《【用故事解读 MobX 源码(五)】 Observable》

你会发明该类 继续自 Atom 类,所以在邃晓 ObservableValue 之前必需邃晓 Atom

实在在 3.x 版本的时刻,
ObservableValue 继续自
BaseAtom

跟着升级到 4.x 版本,官方以及烧毁了
BaseAtom,直接继续自
Atom 这个类。

4.1、Atom

在 MobX 的天下中,任何可以 存储并治理 状况的对象都是 Atom,故事中的 视察员(ObservableValue 实例)本质上就是 Atom(正确的说,而 ObservableValue 是继续了 Atom 这个基类),Atom实例有两项严重的任务:

  1. 当它的值被运用的时刻,就会触发 reportObserved 要领,在 第一篇文章 的解说中可知,MobX 恰是基于该要领,使得视察员和探长之间竖立关联关联。
  2. 当它的值遭到变动的时刻,将会触发 reportChanged 要领,在第三篇文章 《【用故事解读 MobX源码(三)】 shouldCompute》中可知,基于该要领视察员就可以将 非稳态信息逐层上传,终究将让探长、会计员从新实行任务。

Atom 类图以下,从中我们看到前面几章中所涉及到的 onBecomeUnobservedonBecomeObservedreportObservedreportChanged 这几个中心要领,它们都来源于 Atom 这个类:
《【用故事解读 MobX 源码(五)】 Observable》

所以说 Atom 是全部 MobX 的基石并不为过,统统的自动化相应机制都是竖立在这个最最基本类之上。正如在大自然中,万物都是由原子(atom)组成的,借此意义, MobX 中的 ”具有相应式的“ 对象都是由这个 Atom 类组成的。
ComputeValue类 也继续自 AtomReaction 类的完成得依托 Atom,因而不难感知 Atom 基本主要性)

4.2、createAtom

理论上你只需竖立一个 Atom 实例就可以融入到 mobx 的相应式系统中,

怎样自身竖立一个 Atom 呢?

MobX 已暴露了一个名为 createAtom 要领,
官方文档 竖立 observable 数据组织和 reactions(回响反映) 给出了竖立一个 闹钟 的例子,仔细解说了该 createAtom 要领的运用:

...
  // 竖立 atom 就可以和 MobX 中心算法交互
  this.atom = createAtom(
      // 第一个参数是 name 属性,轻易后续 
      "Clock",
      // 第二个参数是回调函数,可选,当 atom 从 unoberved 状况转变到 observed 
      () => this.startTicking(),
      // 第三个参数也是回调函数,可选,与第二个参数对应,此回调是当 atom 从 oberved 状况转变到 unobserved 时会被挪用
      // 注重到,同一个 atom 有能够会在 oberved 状况和 unobserved 之间屡次转换,所以这两个回调有能够会屡次被挪用
      () => this.stopTicking()
  );
...

同时文中也给出了对应的最好实践:

  • 最好给竖立的 Atom 起一个名字,轻易后续 debug
  • onBecomeObservedonBecomeUnobserved 和我们面向对象中组织函数与析构函数的作用类似,轻易举行资本的申请和开释

不过 Atom 实例这个照样倾向底层完成层,除非须要强自定义的特别场景中,日常平凡我们引荐直接运用 observable 或许 observable.box 来竖立视察值越发简朴直接;

4.3、邃晓 ObservableValue

MobX 在 Atom 类基本上,泛化出一个名为 ObservableValue 类,就是我们耳熟能详的 视察值 了。从代码层面上来看,完成 ObservableValue 实在就是继续一下 Atom 这个类,然后再增加许多辅佐的要领和属性就可以了。

邃晓完上述的 Atom 对象以后,你就已邃晓 ObservableValue 的大部份。接下来就是去邃晓 ObservableValue 比拟 Atom 多出来的属性和要领,我这里并不会全讲,太死板了。只遴选主要的两部份 —— Intercept & Observe 部份 和 enhancer 部份

4.3.1、Intercept & Observe 部份

ObservableValue 类图中除了罕见的 toJSON()toString() 要领以外,有两个要领分外有目共睹 —— intercept()observe 两个要领。

假如把 “对象变动” 作为事宜,那末我们可以在 事宜发作之前事宜要领以后 这两个 “切面” 离别可以安插回调函数(callback),轻易顺序动态扩大,这属于 面向切面编程的头脑

不相识 AOP 的,可以查阅
知乎问答-什么是面向切面编程AOP?

在 MobX 天下里,将安插在 事宜发作之前 的回调函数称为 intercept,将安插在 事宜发作以后 的回调函数称为 observe。邃晓这两个要领可以去看 官方中的示例,能疾速体味其作用。

这里轻微进一步讲仔细一些,有时刻官方文档会中把 intercept 邃晓成 拦截器。 这是因为它作用于事宜(数据变动)发作之前,因而可以支配变动的数据内容,以至可以经由过程返回 null 疏忽某次数据变化而不让它见效。

其作用机制也很直接,该要领挪用的终究都是挪用实例的 intercept 要领,如许每次在值变动之前(以下 prepareNewValue 要领实行),都邑触发视察值上所绑定的统统的 拦截器

ObservableValue.prototype.prepareNewValue = function(newValue) {
  ...
  if (hasInterceptors(this)) {
    var change = interceptChange(this, {
      object: this,
      type: 'update',
      newValue: newValue
    });
    if (!change) return UNCHANGED;
    newValue = change.newValue;
  }
  // apply modifier
  ...
};

偏重内里的那行语句 if (!change) return UNCHANGED; ,假如你在 intercept 安插的回调中返回 null 的话,相当于示知 MobX 数值没有变动(UNCHANGED),既然值没有变动,后续的逻辑就不会触发了。

observe 的作用是将回调函数安插在值变动以后(以下 setNewValue 要领挪用),同样是经由过程挪用 notifyListeners 关照统统的监听器

ObservableValue.prototype.setNewValue = function(newValue) {
  ...
  this.reportChanged();
  if (hasListeners(this)) {
    notifyListeners(this, {
      type: 'update',
      object: this,
      newValue: newValue,
      oldValue: oldValue
    });
  }
};

==========【以下是分外的学问内容,可跳过,不影响主线解说】===========

怎样消除安插的回调函数?

Intercept & Observe 这两个函数返回一个 disposer 函数,这个函数是 解绑函数,挪用该函数就可以作废拦截器或许监听器 了。这里有一个最好实践,假如不须要某个拦截器或许监听器了,记得要实时清算自身绑定的监听函数 永久要清算 reaction —— 即挪用 disposer 函数。

那末怎样完成 disposer 解绑函数这套机制?

以拦截器(intercept)为例,注册的时刻挪用 registerInterceptor 要领:

function registerInterceptor(interceptable, handler) {
  var interceptors =
    interceptable.interceptors || (interceptable.interceptors = []);
  interceptors.push(handler);
  return once(function() {
    var idx = interceptors.indexOf(handler);
    if (idx !== -1) interceptors.splice(idx, 1);
  });
}

团体的逻辑比较清楚,就是将传入的 handler(拦截器)增加到 interceptors 数组属性中。症结是在于返回值,返回的是一个闭包 —— once 函数挪用的效果值。

所以我们简化一下 disposer 解绑函数的定义:

disposer = once(function() {
  var idx = interceptors.indexOf(handler);
  if (idx !== -1) interceptors.splice(idx, 1);
});

恰是这个 once 函数是完成解绑功用的中心

检察这个 once 函数源码只要寥寥几行,却将闭包的精华运用到适可而止。

function once(func) {
  var invoked = false;
  return function() {
    if (invoked) return;
    invoked = true;
    return func.apply(this, arguments);
  };
}

once 要领实在经由过程 invoked 变量,掌握传入的 func 函数只挪用一次。

回过头来 disposer 解绑函数,挪用一次就会从 interceptors 数组中移除当前拦截器。运用 once 函数后,你不管挪用多少次 disposer 要领,终究都只会解绑一次。

因为 once 是纯函数,因而大伙儿可以提取出来运用到自身的代码库中 —— 这也是源码浏览的好处之一,自创源码中优异部份,然后进修吸取,引认为用。

=======================================================

4.3.2、enhancer 部份

这部份是在 ObservableValue 组织函数中发挥作用的,其影响的恰恰是最中心的数据属性:

function ObservableValue(value, enhancer, name, notifySpy) {
      ...
      _this.enhancer = enhancer;
      _this.value = enhancer(value, undefined, name);
      ...
    }

在上一篇文章《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》中有提及过 enhance,在那里我们提及过 enhance 实在就是装潢器(decorator)的有用成分,该有用成分影响的恰是本节所讲的 ObservableValue 对象。连系 types/modifier.ts 中有种种 Enhancer 的仔细内容,就可以大抵相识 enhancer 是怎样起到 转换数值 的作用的,以罕见的 deepEnhancer 为例,当在组织函数中实行 _this.value = enhancer(value, undefined, name); 的时刻会进入到 deepEnhance 函数体内:

function deepEnhancer(v, _, name) {
  // it is an observable already, done
  if (isObservable(v)) return v;
  // something that can be converted and mutated?
  if (Array.isArray(v))
    return observable.array(v, {
      name: name
    });
  if (isPlainObject(v))
    return observable.object(v, undefined, {
      name: name
    });
  if (isES6Map(v))
    return observable.map(v, {
      name: name
    });
  return v;
}

这段代码是不是素昧平生?!没错,和上一节所述 createObservable 要领险些一样,采纳 战略设想形式 挪用差异仔细转换函数(比方 observable.object 等)。

如今应当可以邃晓,第一部份的 createObservable 和 第二部份的 observable.box 都是竖立在第三部份之上,而且经由过程第一部份、第二部份以及第三部份取得的视察值对象都是属于视察值对象(ObservableValue),迥然差异,顶多只是“外形”有稍微的差异。

经由过程该 enhancer 部份的解说,我们发明统统待剖析的主要部份都聚焦到第三部份的 observable.object 等这些个转换要领身上了。

5、第三部份:observable.object

因为组织的缘由,上面先讲了最下层的 ObservableValue 部份,如今回来说的 observable.object 要领。从这里你能也许体味到 MobX 系统中递归征象new ObservableValue 内里会挪用 observable.object 要领,从背面的解说里你将会看到 observable.object 要领内里也会挪用 new ObservableValue 的支配,所以 递归地将对象转换成可视察值 就很水到渠成。

浏览官方文档 Observable.object,该 observable.object 要领就是把一个一般的 JavaScript 对象的统统属性都将被拷贝至一个克隆对象并将克隆对象转变成可视察的,而且 observable 是 递归运用 的。

observable.object 等要领对应于上述故事中的 科室 部份,用于实行仔细的支配。罕见的 object 科室是将 plan object 范例数据转换成可视察值,map 科室是将 map 范例数据转换成可视察值….

我们查阅 observable.object(object) 源码,实在就 2 行有用代码:

object: function(props, decorators, options) {
  if (typeof arguments[1] === 'string')
    incorrectlyUsedAsDecorator('object');
  var o = asCreateObservableOptions(options);
  return extendObservable({}, props, decorators, o);
},

可以说 observable.object(object) 实际上是 extendObservable({}, object) 的别号,从这里 extendObservable 要领的第一个参数是 {} 可以看到,终究发生的视察值对象是基于全新的对象,不影响原始传入的对象内容

5.1、extendObservable 要领

讲到这里,会有一种豁然开朗,本来 extendObservable 要领才是终究大 boss,统统视察值的竖立终归走到这个函数。检察该要领的 源码,函数署名以下:

extendObservable(target, properties, decorators, options)
  • 必需吸收 2 ~ 4 个参数
  • 第一个参数必需是对象,比方 bankUser
  • 第二个参数是属性名,比方 name
  • 第三个参数是 装潢器 设置项,这一学问点在上一篇章已解说。
  • 第四个参数是设置选项对象

要领仔细的运用说明参考 官方文档 extendObservable

《【用故事解读 MobX 源码(五)】 Observable》

将该要领的骨干找出来:

function extendObservable(target, properties, decorators, options) {
  ...
  
  // 第一步 挪用 asObservableObject 要领给 target 增加 $mobx 属性
  options = asCreateObservableOptions(options);
  var defaultDecorator =
    options.defaultDecorator ||
    (options.deep === false ? refDecorator : deepDecorator);
  asObservableObject(
    target,
    options.name,
    defaultDecorator.enhancer
  ); 
  
  // 第二步 轮回遍历,将属性经由 decorator(装潢器) 革新后增加到 target 上
  startBatch();
  for (var key in properties) {
    var descriptor = Object.getOwnPropertyDescriptor(
      properties,
      key
    );
    var decorator =
      decorators && key in decorators
        ? decorators[key]
        : descriptor.get
          ? computedDecorator
          : defaultDecorator;
    var resultDescriptor = decorator(
      target,
      key,
      descriptor,
      true
    );
    if (resultDescriptor){
      Object.defineProperty(target, key, resultDescriptor);
    }
  }
  endBatch();
  return target;

这要领看上去块头很大,不过剖析起来就 2 大步:

  • 起首挪用 asObservableObject 要领,给 target 天生 $mobx 属性
  • 其次挨个让每一个属性经由 decorator 革新后从新安装到 target 上,默许的 decorator 是 deepDecorator,装潢器的寄义和作用在上一篇文章已讲过,点击 这里 温习

5.2、第一步:挪用 asObservableObject

asObservableObject 要领,主假如给目的对象天生 $mobx 属性;该 $mobx 属性对应上述故事中的 科长 角色,用于治理对象的读写支配。

为何要增加 $mobx 属性?其仔细作用又是什么?

经由过程浏览源码,我无从获知作者增加 $mobx 属性的来由,但可以晓得 $mobx 的作用是什么。

起首,$mobx 属性是一个 ObservableObjectAdministration 对象,类图以下:
《【用故事解读 MobX 源码(五)】 Observable》

用例子来看看 $mobx 属性:

var bankUser = observable({
    income: 3,
    name: '张三'
});

console.table(bankUser);

下图红框处标示出来的就是 bankUser.$mobx 属性:
《【用故事解读 MobX 源码(五)】 Observable》

我们进一步经由过程以下两行代码输出 $mobx 属性中仔细的数据成员和具有的要领成员:

console.log(`bankUser.$mobx:`, bankUser.$mobx);
console.log(`bankUser.$mobx.__proto__:`, bankUser.$mobx.__proto__);

《【用故事解读 MobX 源码(五)】 Observable》

在这么多属性中,分外须要注重的是 writeread 这两个要领,这两个要领算是 $mobx 属性的魂魄,下面行将会讲到,这里先点名一下。

除此以外还须要关注 $mobx 对象中的 values 属性,刚初始化的时刻该属性是 {} 空对象,不过注重上面截图中看到 $mobx.values 是有内容的,这实在不是在这一步完成,而是在接下来要讲的第二步中所构成的。

你可以这么邃晓,这一步仅仅是找到担负科长的人选,照样光杆司令;下一步才是正式委派科长到某个科室,那个时刻新上任的科长才有权利牵制其部属的视察员。

5.3、第二步:每一个属性都经由一遍 decorator 的 “浸礼”

这部份就是运用 装潢器 支配了,默许是运用 deepDecorator 这个装潢器。装潢器的运用流程在 上一篇文章 中有仔细解说,直接拿结论过来:

《【用故事解读 MobX 源码(五)】 Observable》

你会发明运用装潢器的末了一步是在挪用 defineObservableProperty 要领时竖立 ObservableValue 属性,对应在 defineObservableProperty 源码 中以下语句:

var observable = (adm.values[propName] = new ObservableValue(
  newValue,
  enhancer,
  adm.name + '.' + propName,
  false
));

这里的 adm 就是 $mobx 属性,如许新天生的 ObservableValue 实例就挂载在 $mobx.values[propName] 属性下。

如许的设定很奇妙,值得我们深挖。先看一下下面的示例:

var user = {
  income: 3,
  name: '张三'
};
var bankUser = observable(user);

bankUser.income = 5;

console.log(bankUser.income);
console.table(bankUser.$mobx.values.income);

在这个案例中,我们直接修正 bankUserincome 属性为 5,一旦修正,此时 bankUser.$mobx.values.income 也会同步修正:
《【用故事解读 MobX 源码(五)】 Observable》

这是怎样做到的呢?

答案是:经由过程 generateObservablePropConfig 要领

function generateObservablePropConfig(propName) {
  return (
    observablePropertyConfigs[propName] ||
    (observablePropertyConfigs[propName] = {
      configurable: true,
      enumerable: true,
      get: function() {
        return this.$mobx.read(this, propName);
      },
      set: function(v) {
        this.$mobx.write(this, propName, v);
      }
    })
  );
}

该要领是作用在 decorator 装潢器其作用时期,用 generateObservablePropConfig 天生的形貌符重写原始对象的形貌符,仔细看形貌符里的 getset 要领,对象属性的 读写离别映照到 $mobx.read$mobx.write这两个要领中

在这里,我们就可以晓得挂载 $mobx 属性的企图:MobX 为我们竖立了原对象属性的 镜像 支配,统统针对原有属性的读写支配都将镜像复刻到 $mobx.values 对应 Observable 实例对象上,从而将庞杂的支配隐蔽起来,给用户供应直观简朴的,进步用户体验

以赋值语句 bankUser.income = 5 为例,如许的赋值语句我们日常平凡常常写,只不过这里的 bankUser 是我们 observable.object 支配获得的,所以 MobX 会同步修正 bankUser.$mobx.values.income 这个 ObservableValue 实例对象,从而触发 reportChanged 或许 reportObserved 等要领,开启 相应式链 的第一步。

你所做的支配和以往一样,誊写 bankUser.income = 5 如许的语句就可以。而实际上 mobx 在背地默默地做了许多事情,如许就将简朴的支配留给用户,而把绝大多数庞杂的处置惩罚都隐蔽给 MobX 框架来处置惩罚了。

5.4、递归完成视察值

本小节最先已提及过递归通报视察值,这里再从代码层面看一下 递归完成视察值 的道理。这一步是在 decorator 装潢器运用历程当中,经由过程 $mobx 挂载对应属性的 ObservableValue 实例到达的。

对应的支配在适才的 5.3 已讲过,照样在 defineObservableProperty 源码 那行代码:

var observable = (adm.values[propName] = new ObservableValue(
  newValue,
  enhancer,
  adm.name + '.' + propName,
  false
));

以下述的 parent 对象为例:

var parent = {
  child: {
    name: 'tony'
  }
}

当我们实行 observable(parent)(或许 new ObservableValue(parent)observable.box(parent) 等竖立视察值的要领),实在行途径以下:

《【用故事解读 MobX 源码(五)】 Observable》

从上图就可以看到,在 decorator 那一步将属性转换成 ObservableValue 实例,如许在团体上看就是递归完成了视察值的转换 —— 把 child 和它部属的属性也转换成可视察值。

6、小测试

请剖析 observable.mapobservable.array 的源码,看看它们和 observable.object 要领之间的差异在哪儿。

7、总结

本文重点是讲 Observable 类,与之相干的类图整顿以下:

《【用故事解读 MobX 源码(五)】 Observable》

  • ObservableValue 继续自 Atom,并完成一系列的 接口
  • ObservableObjectAdministration镜像支配治理者,它主要经由过程 $mobx 属性来操控治理每一个视察值 ObservableValue
  • 比较主要的要领是 interceptobserve ,用“面向瘦语”编程的术语来说,这两个要领就是两个 瘦语,离别作用于数值变动前后,轻易针对数据状况做一系列的相应;

本文中涌现许多 observable 相干的单词,稍作总结:

  • ObservableValue 是一个一般的 class,用于示意 视察值 这个观点。
  • observable 是一个函数,也是 mobx 供应的 API,即是 createObservable,代表支配,该支配历程当中会依据状况挪用 observable.object(或许 observable.arrayobservable.map)等要领,终究目的是为了竖立 ObservableValue 对象。
  • extendObservable,这是一个东西函数,算是比较底层的要领,该要领用来向已存在的目的对象增加 observable 属性;上述的 createObservable 要领实在也是借用该要领完成的;

MobX 默许会递归将对象转换成可视察属性,这主假如得益于 enhancer 在个中发挥的作用,因为每一次 Observable 组织函数会对传入的值经由 enhancer 处置惩罚;

有人不禁会问,既然供应 observable 要领了,那末 observable.box 要领存在的意义是什么?答案是,因为它直接返回的是 ObservableValue,它比拟一般的 observable 竖立的视察值,供应越发细粒度(底层)的支配;

比方它除了能像一般视察值一样和 autorun 搭配运用以外,竖立的对象还直接具有 interceptobserve 要领:

var pr1 = observable.box(2);
autorun(() => {
  console.log('value:', pr1.get());
});
pr1.observe(change => {
  console.log('change from', change.oldValue, 'to', change.newValue);
});

pr1.set(3);

// 以下是输出效果:
// value: 2
// value: 3
// change from 2 to 3

固然 MobX 斟酌也很全面,还零丁供应 Intercept & Observe 两个东西函数,以函数挪用的体式格局给视察值新增这两种回调函数。

因而下述两种体式格局是同等的,可以自身实验一下:

// 挪用 observe 属性要领
pr1.observe(change => {
  console.log('change from', change.oldValue, 'to', change.newValue);
});

// 运用 observe 东西函数可以到达雷同的目的
observe(pr1, change => {
    console.log('change from', change.oldValue, 'to', change.newValue);
}):


本文针对 MobX 4 源码解说,而在 MobX 5 版本中的 Observable 类则是采纳 proxy 来完成 Observable,团体思绪和上述的并没有二致,只是在细节方面将 Object.defineProperty 替换成 new Proxy 的写法罢了,感兴趣的同砚发起先浏览 《抱歉,学会 Proxy 真的可以随心所欲》相识 Proxy 的写法,然后去看一下 MobX 5 中的 observable.object 要领已改用 createDynamicObservableObject 来竖立 proxy,所竖立的 proxy 模子来自于 objectProxyTraps 要领;若有时机将在后续的文章中更新这方面的学问。

用故事解说 MobX 源码的系列文章至此告一段落,后续以散篇的情势宣布跟 MobX 相干的文章。

下面的是我的民众号二维码图片,迎接关注,实时猎取最新技术文章。
《【用故事解读 MobX 源码(五)】 Observable》

    原文作者:JSCON简时空
    原文地址: https://segmentfault.com/a/1190000015875144
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞