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

================前言===================html

=======================================git

A. Story Time

最高警长看完执行官(MobX)的自动部署方案,对 “观察员” 这个基层人员工做比较感兴趣,自执行官拿给他部署方案的时候,他就注意到全部上层人员的功能都是基于该底层人员高效的工做机制;github

次日,他找上执行官(MobX)一块儿去视察“观察员”所在机构部门(下面简称为 ”观察局“),想更深刻地了解 “观察员” 运行分配机制。算法

observable

当最高警长到达部门的时候,刚好遇到该部门刚好要开始执行 MobX 前不久新下发的任务,要求监控 parent 对象的一举一动:编程

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

var bankUser = observable(parent);

任务达到观察局办公室后,相应的办公室文员会对任务进行分析,而后会依据对象类型交给相应科室进行处理,常见的有 object 科,另外还有 map 科和 array 科;segmentfault

如今,办公室文员见传入的对象是 parent 是个对象,就将其传递给 object 科,让其组织起一块儿针对该 parent 对象的 ”观察小组“,组名为 bankUser设计模式

object 科接到任务,委派某位科长(如下称为 bankUser 科长)组成专项负责此 parent 对象的观察工做,bankUser 科长接手任务后发现有两个属性,其中 child 是对象类型,age 是原始值类型,就分别将任务委派给 child 小科长 和 name 观察员 O1,child 小科长接到任务后再委派给 name 观察员 O2 和 age 观察员 O3,最终执行该任务的人员结构以下:api

structor

观察员的任务职责咱们已经很熟悉了,当读写观察员对应的数据时将触发 reportObservedpropagateChanged 方法;数组

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

科长的人物职责是起到 管理 做用,它负责统管在他名下的观察员。好比当咱们读写 bankUser.child 对象的 name 属性时(好比执行语句 bankUser.child.name = 'Jack'),首先感知到读写操做的并不是是 观察员 O2 而是bankUser科长bankUser科长会告知 child 小科长有数据变动,child 小科长而后再将信息传达给 name 观察员 O2 ,而后才是观察员 O2 对数据读写起反应,这才让观察员 O2 发挥做用。

transform

从代码层面看,咱们看到仅仅是执行 bankUser.child.name = 'Jack'这一行语句,和咱们日常修改对象属性并没有二致。然而在这一行代码背后其实牵动了一系列的操做。这实际上是 MobX 构建起的一套 ”镜像“ 系统,使用者仍旧按平时的方式读写对象,然而每一个属性的读写操做实则都镜像到观察局 的某个小组具体的操做;很是相似于古代的 ”垂帘听政“ ,看似皇帝坐在文武百官前面,其实真正作出决策响应的是藏在帘后面的那我的。

前几章中咱们只看到观察员在活动,然则背后离不开 科长 这一角色机制在背后暗暗的调度。对每项任务,最终都会落实到观察员采起“一对一”模式监控分配到给本身的观察项,而每一个观察员确定是隶属于某个 ”科长“ 带领。在 MobX 系统里,办公室、科长和观察员是密不可分,共同构建起 观察局 运行体制;

"分工明确,运转高效",这是最高警长在巡视完观察员培训基地后的第一印象,观察局运转的每一步的设计都有精细的考量;

B. Source Code Time

先罗列本文故事中人物与 MobX 源码概念映射关系:

故事人物 MobX 源码 解释
警署最高长官 (无) MobX 用户,没错,就是你
执行官 MobX MobX 整个 MobX 运行环境
观察局办公室(主任、文员) observableobservable.box 用于建立 Observable 的 API
object 科室、map 科室、array 科室 observable.objectobservable.mapobservable.array 将不一样复合类型转换成观察值的方法
科长 ObservableObjectAdministration 主要给对象添加 $mobx 属性
观察员 ObservableValue 实例 ObservableValue 实例

一、总入口: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 这二者:
ob value

文字比较枯燥,用图来表示就是下面那样子:

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

box and object
能够发现 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 方法实现原始值转换:

box can

二、第一部分: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):设计模式之策略模式

用图来展现一下具体的走向:

trend

  • 走向 ① 是 装饰器语法所特有的,这是由于此时传给 createObservable 的第二个参数是 string 类型,这一点咱们在上一篇文章有详细论述;
  • 走向 ② 很直观,若是传入的参数就已是 观察值 了,很少废话直接返回传入的值,不须要转换;
  • 走向 ③、④ 、⑤ 是直根据传入参数的类型分别调用具针对具体类型的转换方法;
  • 走向 ⑥,在上面示例中咱们已经看到过, 针对原始值会提示建议用户使用 observable.box 方法。

第一部分的 createObservable 的内容就那么些,总之只是起了 “向导” 做用。是否是比你想象中的要简单?

接下来咱们继续看第二部分的 observable.box 方法。

三、第二部分:observable.box

这个方法对应上述故事中的 观察局办公室文员 角色,也是属于办公室部门的,所起到的做用和 主任 大同小异,只是平时咱们用得并很少罢了。

当我第一次阅读 官网文档 中针对有关 observable.box 的描述时:

box

来回读了几回,“盒子”是个啥?它干吗用的? “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 对象而已。

四、核心类:ObservableValue

总算是讲到这个 ObservableValue 类了,该类是理解可观察值的关键概念。这个类对应上述故事中的 观察员 角色,就是最基层的 name 观察员 O一、O二、O3 那些。

本篇文章的最终目的也就是为了讲清楚这个 ObservableValue 类,其余的概念反而是围绕它而建立起来的。

分析其源码,将这个类的属性和方法都拎出来瞧瞧,绘制成类图大体以下:

gene

你会发现该类 继承自 Atom 类,因此在理解 ObservableValue 以前必须理解 Atom

其实在 3.x 版本的时候, ObservableValue 继承自 BaseAtom
随着升级到 4.x 版本,官方以及废弃了 BaseAtom,直接继承自 Atom 这个类。

4.一、Atom

在 MobX 的世界中,任何可以 存储并管理 状态的对象都是 Atom,故事中的 观察员(ObservableValue 实例)本质上就是 Atom(准确的说,而 ObservableValue 是继承了 Atom 这个基类),Atom实例有两项重大的使命:

  1. 当它的值被使用的时候,就会触发 reportObserved 方法,在 第一篇文章 的讲解中可知,MobX 正是基于该方法,使得观察员和探长之间创建关联关系。
  2. 当它的值受到更改的时候,将会触发 reportChanged 方法,在第三篇文章 《【用故事解读 MobX源码(三)】 shouldCompute》中可知,基于该方法观察员就能够将 非稳态信息逐层上传,最终将让探长、会计员从新执行任务。

Atom 类图以下,从中咱们看到前面几章中所涉及到的 onBecomeUnobservedonBecomeObservedreportObservedreportChanged 这几个核心方法,它们都来源于 Atom 这个类:
Atom

因此说 Atom 是整个 MobX 的基石并不为过,全部的自动化响应机制都是创建在这个最最基础类之上。正如在大天然中,万物都是由原子(atom)构成的,借此意义, MobX 中的 ”具有响应式的“ 对象都是由这个 Atom 类构成的。
ComputeValue类 也继承自 AtomReaction 类的实现得依靠 Atom,所以不难感知 Atom 基础重要性)

4.二、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.三、理解 ObservableValue

MobX 在 Atom 类基础上,泛化出一个名为 ObservableValue 类,就是咱们耳熟能详的 观察值 了。从代码层面上来看,实现 ObservableValue 其实就是继承一下 Atom 这个类,而后再添加许多辅助的方法和属性就能够了。

理解完上述的 Atom 对象以后,你就已经理解 ObservableValue 的大部分。接下来就是去理解 ObservableValue 相比 Atom 多出来的属性和方法,我这里并不会全讲,太枯燥了。只挑选重要的两部分 —— Intercept & Observe 部分 和 enhancer 部分

4.3.一、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.二、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 等这些个转换方法身上了。

五、第三部分: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.一、extendObservable 方法

讲到这里,会有一种恍然大悟,原来 extendObservable 方法才是最终大 boss,一切观察值的建立终归走到这个函数。查看该方法的 源码,函数签名以下:

extendObservable(target, properties, decorators, options)
  • 必须接收 2 ~ 4 个参数
  • 第一个参数必须是对象,好比 bankUser
  • 第二个参数是属性名,好比 name
  • 第三个参数是 装饰器 配置项,这一知识点在上一篇章已经讲解。
  • 第四个参数是配置选项对象

方法具体的使用说明参考 官方文档 extendObservable

deco

将该方法的主干找出来:

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.二、第一步:调用 asObservableObject

asObservableObject 方法,主要是给目标对象生成 $mobx 属性;该 $mobx 属性对应上述故事中的 科长 角色,用于管理对象的读写操做。

为何要添加 $mobx 属性?其具体做用又是什么?

经过阅读源码,我无从获知做者添加 $mobx 属性的理由,但能够知道 $mobx 的做用是什么。

首先,$mobx 属性是一个 ObservableObjectAdministration 对象,类图以下:
class

用例子来看看 $mobx 属性:

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

console.table(bankUser);

下图红框处标示出来的就是 bankUser.$mobx 属性:
$mobx 属性

咱们进一步经过如下两行代码输出 $mobx 属性中具体的数据成员和拥有的方法成员:

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

$mobx

在这么多属性中,格外须要注意的是 writeread 这两个方法,这两个方法算是 $mobx 属性的灵魂,下面即将会讲到,这里先点名一下。

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

你能够这么理解,这一步仅仅是找到担任科长的人选,仍是光杆司令;下一步才是正式委派科长到某个科室,那个时候新上任的科长才有权力管束其下属的观察员。

5.三、第二步:每一个属性都通过一遍 decorator 的 “洗礼”

这部分就是应用 装饰器 操做了,默认是使用 deepDecorator 这个装饰器。装饰器的应用流程在 上一篇文章 中有详细讲解,直接拿结论过来:

flow

你会发现应用装饰器的最后一步是在调用 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 也会同步修改:
values

这是怎么作到的呢?

答案是:经过 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.四、递归实现观察值

本小节开始已经说起过递归传递观察值,这里再从代码层面看一下 递归实现观察值 的原理。这一步是在 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) 等建立观察值的方法),其执行路径以下:

step

从上图就能够看到,在 decorator 那一步将属性转换成 ObservableValue 实例,这样在总体上看就是递归完成了观察值的转换 —— 把 child 和它下属的属性也转换成可观察值。

六、小测试

请分析 observable.mapobservable.array 的源码,看看它们和 observable.object 方法之间的差异在哪儿。

七、总结

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

class

  • 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 相关的文章。

下面的是个人公众号二维码图片,欢迎关注,及时获取最新技术文章。
微信公众号