================前言===================html
本系列文章:react
=======================================git
最高警长看完执行官(MobX)的自动部署方案,对 “观察员” 这个基层人员工做比较感兴趣,自执行官拿给他部署方案的时候,他就注意到全部上层人员的功能都是基于该底层人员高效的工做机制;github
次日,他找上执行官(MobX)一块儿去视察“观察员”所在机构部门(下面简称为 ”观察局“),想更深刻地了解 “观察员” 运行分配机制。算法
当最高警长到达部门的时候,刚好遇到该部门刚好要开始执行 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
观察员的任务职责咱们已经很熟悉了,当读写观察员对应的数据时将触发 reportObserved
或 propagateChanged
方法;数组
这里涉及到两位科长(bankUser
科长 和 child
小科长),那么科长的任务职责是什么呢?
科长的人物职责是起到 管理 做用,它负责统管在他名下的观察员。好比当咱们读写 bankUser.child
对象的 name
属性时(好比执行语句 bankUser.child.name = 'Jack'
),首先感知到读写操做的并不是是 观察员 O2 而是bankUser
科长,bankUser
科长会告知 child
小科长有数据变动,child
小科长而后再将信息传达给 name
观察员 O2 ,而后才是观察员 O2 对数据读写起反应,这才让观察员 O2 发挥做用。
从代码层面看,咱们看到仅仅是执行 bankUser.child.name = 'Jack'
这一行语句,和咱们日常修改对象属性并没有二致。然而在这一行代码背后其实牵动了一系列的操做。这实际上是 MobX 构建起的一套 ”镜像“ 系统,使用者仍旧按平时的方式读写对象,然而每一个属性的读写操做实则都镜像到观察局 的某个小组具体的操做;很是相似于古代的 ”垂帘听政“ ,看似皇帝坐在文武百官前面,其实真正作出决策响应的是藏在帘后面的那我的。
前几章中咱们只看到观察员在活动,然则背后离不开 科长 这一角色机制在背后暗暗的调度。对每项任务,最终都会落实到观察员采起“一对一”模式监控分配到给本身的观察项,而每一个观察员确定是隶属于某个 ”科长“ 带领。在 MobX 系统里,办公室、科长和观察员是密不可分,共同构建起 观察局 运行体制;
"分工明确,运转高效",这是最高警长在巡视完观察员培训基地后的第一印象,观察局运转的每一步的设计都有精细的考量;
先罗列本文故事中人物与 MobX 源码概念映射关系:
故事人物 | MobX 源码 | 解释 |
---|---|---|
警署最高长官 | (无) | MobX 用户,没错,就是你 |
执行官 MobX | MobX | 整个 MobX 运行环境 |
观察局办公室(主任、文员) | observable、observable.box 等 | 用于建立 Observable 的 API |
object 科室、map 科室、array 科室 |
observable.object、observable.map、observable.array | 将不一样复合类型转换成观察值的方法 |
科长 | ObservableObjectAdministration | 主要给对象添加 $mobx 属性 |
观察员 | ObservableValue 实例 | ObservableValue 实例 |
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
的属性的确来自于createObservable
和 observableFactories
这二者:
文字比较枯燥,用图来表示就是下面那样子:
这里我大体划分了一下,分红 4 部份内容来理解:
createObservable
方法刚才粗略讲过,是 MobX API 的 observable 的别名,是一个高度封装的方法,算是一个总入口,方便用户调用;该部分对应上述故事中的 观察局办公室主任 的角色box
是一个转换函数,用于将 原值(primitive value) 直接转换成 ObservableValue 对象;shallowBox
是 box
函数的非 deep 版本;该部分对应上述故事中的 观察局办公室文员 的角色;shallow
的版本;该部分对应上述故事中的 科室 部分;如何理解这 4 部分的以前的关系呢?我我的的理解以下:
box
函数转换能力反而比第一部分更广,支持将原始值转换成可观察值;下面咱们看两个具体的示例,来辅助消化上面的结论。
示例一: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);
能够发现 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 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):设计模式之策略模式
用图来展现一下具体的走向:
createObservable
的第二个参数是 string 类型,这一点咱们在上一篇文章有详细论述;observable.box
方法。第一部分的 createObservable
的内容就那么些,总之只是起了 “向导” 做用。是否是比你想象中的要简单?
接下来咱们继续看第二部分的 observable.box
方法。
这个方法对应上述故事中的 观察局办公室文员 角色,也是属于办公室部门的,所起到的做用和 主任
大同小异,只是平时咱们用得并很少罢了。
当我第一次阅读 官网文档 中针对有关 observable.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
类了,该类是理解可观察值的关键概念。这个类对应上述故事中的 观察员 角色,就是最基层的 name
观察员 O一、O二、O3 那些。
本篇文章的最终目的也就是为了讲清楚这个 ObservableValue 类,其余的概念反而是围绕它而建立起来的。
分析其源码,将这个类的属性和方法都拎出来瞧瞧,绘制成类图大体以下:
你会发现该类 继承自 Atom 类,因此在理解 ObservableValue
以前必须理解 Atom
。
其实在 3.x 版本的时候,ObservableValue
继承自BaseAtom
;
随着升级到 4.x 版本,官方以及废弃了BaseAtom
,直接继承自Atom
这个类。
在 MobX 的世界中,任何可以 存储并管理 状态的对象都是 Atom,故事中的 观察员(ObservableValue 实例)本质上就是 Atom(准确的说,而 ObservableValue
是继承了 Atom
这个基类),Atom
实例有两项重大的使命:
Atom
类图以下,从中咱们看到前面几章中所涉及到的 onBecomeUnobserved
、onBecomeObserved
、reportObserved
、reportChanged
这几个核心方法,它们都来源于 Atom
这个类:
因此说 Atom 是整个 MobX 的基石并不为过,全部的自动化响应机制都是创建在这个最最基础类之上。正如在大天然中,万物都是由原子(atom)构成的,借此意义, MobX 中的 ”具有响应式的“ 对象都是由这个 Atom
类构成的。
(ComputeValue
类 也继承自 Atom
,Reaction
类的实现得依靠 Atom
,所以不难感知 Atom
基础重要性)
理论上你只要建立一个 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() ); ...
同时文中也给出了对应的最佳实践:
onBecomeObserved
和 onBecomeUnobserved
和咱们面向对象中构造函数与析构函数的做用类似,方便进行资源的申请和释放。不过 Atom 实例这个仍是偏向底层实现层,除非须要强自定义的特殊场景中,平时咱们推荐直接使用 observable
或者 observable.box
来建立观察值更为简单直接;
MobX 在 Atom
类基础上,泛化出一个名为 ObservableValue
类,就是咱们耳熟能详的 观察值 了。从代码层面上来看,实现 ObservableValue
其实就是继承一下 Atom
这个类,而后再添加许多辅助的方法和属性就能够了。
理解完上述的 Atom
对象以后,你就已经理解 ObservableValue
的大部分。接下来就是去理解 ObservableValue
相比 Atom
多出来的属性和方法,我这里并不会全讲,太枯燥了。只挑选重要的两部分 —— Intercept & Observe 部分 和 enhancer 部分
在 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
是纯函数,所以大伙儿能够提取出来运用到本身的代码库中 —— 这也是源码阅读的益处之一,借鉴源码中优秀部分,而后学习吸取,引觉得用。
=======================================================
这部分是在 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
等这些个转换方法身上了。
由于结构的缘由,上面先讲了最基层的 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
方法的第一个参数是 {}
能够看到,最终产生的观察值对象是基于全新的对象,不影响原始传入的对象内容。
讲到这里,会有一种恍然大悟,原来 extendObservable
方法才是最终大 boss,一切观察值的建立终归走到这个函数。查看该方法的 源码,函数签名以下:
extendObservable(target, properties, decorators, options)
bankUser
name
方法具体的使用说明参考 官方文档 extendObservable
将该方法的主干找出来:
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
,装饰器的含义和做用在上一篇文章已讲过,点击 这里 复习asObservableObject
方法,主要是给目标对象生成 $mobx
属性;该 $mobx
属性对应上述故事中的 科长 角色,用于管理对象的读写操做。
为何要添加 $mobx
属性?其具体做用又是什么?
经过阅读源码,我无从获知做者添加 $mobx
属性的理由,但能够知道 $mobx
的做用是什么。
首先,$mobx
属性是一个 ObservableObjectAdministration
对象,类图以下:
用例子来看看 $mobx
属性:
var bankUser = observable({ income: 3, name: '张三' }); console.table(bankUser);
下图红框处标示出来的就是 bankUser.$mobx
属性:
咱们进一步经过如下两行代码输出 $mobx
属性中具体的数据成员和拥有的方法成员:
console.log(`bankUser.$mobx:`, bankUser.$mobx); console.log(`bankUser.$mobx.__proto__:`, bankUser.$mobx.__proto__);
在这么多属性中,格外须要注意的是 write
和 read
这两个方法,这两个方法算是 $mobx
属性的灵魂,下面即将会讲到,这里先点名一下。
除此以外还须要关注 $mobx
对象中的 values
属性,刚初始化的时候该属性是 {}
空对象,不过注意上面截图中看到 $mobx.values
是有内容的,这其实不是在这一步完成,而是在接下来要讲的第二步中所造成的。
你能够这么理解,这一步仅仅是找到担任科长的人选,仍是光杆司令;下一步才是正式委派科长到某个科室,那个时候新上任的科长才有权力管束其下属的观察员。
decorator
的 “洗礼”这部分就是应用 装饰器 操做了,默认是使用 deepDecorator
这个装饰器。装饰器的应用流程在 上一篇文章 中有详细讲解,直接拿结论过来:
你会发现应用装饰器的最后一步是在调用 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);
在这个案例中,咱们直接修改 bankUser
的 income
属性为 5
,一旦修改,此时 bankUser.$mobx.values.income
也会同步修改:
这是怎么作到的呢?
答案是:经过 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
生成的描述符重写原始对象的描述符,仔细看描述符里的 get
和 set
方法,对象属性的 读写分别映射到 $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 框架来处理了。
本小节开始已经说起过递归传递观察值,这里再从代码层面看一下 递归实现观察值 的原理。这一步是在 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)
等建立观察值的方法),其执行路径以下:
从上图就能够看到,在 decorator
那一步将属性转换成 ObservableValue
实例,这样在总体上看就是递归完成了观察值的转换 —— 把 child
和它下属的属性也转换成可观察值。
请分析 observable.map
和 observable.array
的源码,看看它们和 observable.object
方法之间的差异在哪儿。
本文重点是讲 Observable
类,与之相关的类图整理以下:
ObservableValue
继承自 Atom
,并实现一系列的 接口;ObservableObjectAdministration
是 镜像操做管理者,它主要经过 $mobx
属性来操控管理每一个观察值 ObservableValue
intercept
和 observe
,用“面向切口”编程的术语来说,这两个方法就是两个 切口,分别做用于数值更改先后,方便针对数据状态作一系列的响应;本文中出现不少 observable
相关的单词,稍做总结:
ObservableValue
是一个普通的 class,用于表示 观察值 这个概念。observable
是一个函数,也是 mobx 提供的 API,等于 createObservable
,表明操做,该操做过程当中会根据状况调用 observable.object
(或者 observable.array
、observable.map
)等方法,最终目的是为了建立 ObservableValue
对象。extendObservable
,这是一个工具函数,算是比较底层的方法,该方法用来向已存在的目标对象添加 observable 属性;上述的 createObservable
方法其实也是借用该方法实现的;MobX 默认会递归将对象转换成可观察属性,这主要是得益于 enhancer
在其中发挥的做用,由于每一次 Observable 构造函数会对传入的值通过 enhancer
处理;
有人不由会问,既然提供 observable
方法了,那么 observable.box
方法存在的意义是什么?答案是,因为它直接返回的是 ObservableValue
,它相比普通的 observable
建立的观察值,提供更加细粒度(底层)的操做;
好比它除了能像正常观察值同样和 autorun
搭配使用以外,建立的对象还直接拥有 intercept
和 observe
方法:
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 相关的文章。
下面的是个人公众号二维码图片,欢迎关注,及时获取最新技术文章。