================前言===================html
本系列文章:react
=======================================git
按照步骤,这篇文章应该写 观察值(Observable)的,不过在撰写的过程当中发现,若是不先搞明白装饰器和 Enhancer(对这个单词陌生的,先不要着急,继续往下看) ,直接去解释观察值(Observable)会很费劲。由于在 MobX 中是使用装饰器设计模式实现观察值的,因此说要先掌握装饰器,才能进一步去理解观察值。es6
因此这是一篇 “插队” 的文章,用于去理解 MobX 中的装饰器和 Enhancer 概念。github
本文主要解决我我的在源码阅读中的疑惑:npm
Enhancer
究竟是个什么概念?它在 MobX 体系中发挥怎样的做用?它和装饰器又是怎么样的一层关系?若是你也有这样的疑惑,不妨继续阅读本文,欢迎一块儿讨论。编程
至于 观察值(Observable),在本文中你只要掌握住 官方文档 observable 的用法就足够了,好比(示例摘自官方文档):json
const person = observable({ firstName: "Clive Staples", lastName: "Lewis" }); person.firstName = "C.S."; const temperature = observable.box(20); temperature.set(25);
对于 observable
方法的源码解析将在下一篇中详细展开,此篇文章不会作过多的讨论。segmentfault
和其余语言(Python、Java)同样,装饰器语法是借助 @
符号实现的,如今问题就归结到如何用 JS 去实现 @
语法。设计模式
对于还不熟悉装饰器语法的读者,这里推荐文章 《ES7 Decorator 装饰者模式》,以钢铁侠为例,经过装备特殊的装备就能将普通人变成钢铁侠,简单归纳起来就是:
装饰器设计模式的理念就和上面那样的朴素,在不改造 托尼·史塔克(Tony Stark) 本体的前提下,经过加装 盔甲、飞行器 的方式加强 Tony 的能力,从而“变成”钢铁侠。
有关装饰器使用的文章,还能够参考这两篇参考文章 探寻 ECMAScript 中的装饰器 Decorator、细说ES7 JavaScript Decorators
文章都比较早,当时写文章的做者都认为在新的 ES7 里会推出标准的 @
语法,然而过后证实官方并无这个意愿。咱们知道目前的 ECMAScript 2015 标准,甚至到 ECMAScript 2018 标准官方都没有提供 @
语法的支持,咱们在其余文章中看到的 @
语法都是经过 babel 插件来实现的。
上面说起的参考文章都是属于应用类型的,就是直接使用装饰器语法(即直接使用 @
语法)来展现装饰器的实际应用,而对于如何实现 @
语法并无说起 —— 那就是如何用 Object.defineProperty 来实现 @
语法。
道理你们都懂,那么到底如何才能本身动手去实现 @
装饰器语法呢?
在 JS 中,咱们借助 Object.defineProperty 方法实现装饰器设计模式,该方法签名以下:
Object.defineProperty(obj, prop, descriptor)
其中最核心的实际上是 descriptor
—— 属性描述符 。
属性描述符总共分两种:数据描述符(Data descriptor)和 访问器描述符(Accessor descriptor)。
描述符必须是两种形式之一,但不能同时是二者。
好比 数据描述符:
Object.getOwnPropertyDescriptor(user,'name'); // 输出 /** { "value": "张三", "writable": true, "enumerable": true, "configurable": true } **/
还有 访问器描述符:
var anim = { get age() { return 5; } }; Object.getOwnPropertyDescriptor(anim, "age"); // 输出 /** { configurable: true, enumerable: true, get: /*the getter function*/, set: undefined } **/
具体可参考 StackOverflow 上的问答 What is a descriptor? ;
接下来,咱们一块儿来看一下 babel 中究竟是如何实现 @
语法的?
在理解属性描述符的基础上,咱们就能够去看看 babel 对于装饰器 @
语法的内部实现了。
就拿 MobX 官方的示例 来说:
import { observable, computed, action } from "mobx"; class OrderLine { @observable price = 0; @observable amount = 1; @computed get total() { return this.price * this.amount; } @action.bound increment() { this.amount++ // 'this' 永远都是正确的 } }
咱们并非真正想要运行上面那段代码,而是想看一下 babel 经过装饰器插件,把上面那段代码中的 @
语法转换成什么样子了。
运行这段代码须要搭建 babel 环境,因此直接扔到浏览器运行会报错的。按照官方文档 如何(不)使用装饰器 中的提示,须要借助 babel-preset-mobx 插件,这是一个预设(preset,至关于 babel 插件集合),真正和装饰器有关的是插件是 babel-plugin-transform-decorators-legacy。
放到 babel 在线工具,粘贴现有的示例代码会报错,不过 babel 给出了友好的提示,由于使用到了装饰器语法,须要安装 babel-plugin-transform-decorators-legacy:
咱们点击左下方的 Add Plugin 按钮,在弹出的搜索框里输入关键字 decorators-legacy,选择这个插件就能够:
选完插件以后,代码就会成功转译:
底下会提示 require is not defined 错误,这个错误并不影响你分析装饰器的语法,由于有 @
符号部分都已经转换成 ES5 语法了,只是这个报错没法让这段示例代码运行起来。
这是由于 Babel 只是将最新的 ES6 语法“翻译”成各大浏览器支持比较好的 ES5 语法,但模块化写法(
require
语句)自己就不是 ECMAScript 的标准,而是产生了其余的模块化写法标准,例如 CommonJS,AMD,UMD。所以 Babel 转码模块化写法后在浏览器中仍是没法运行,此时能够考虑放到 Webpack 这种自动化构建工具环境中,此时 Webpack 是支持模块化写法的
若是有强迫症的同窗,非得想要这段代码运行起来,能够参考下述的 方法二。
官方提供了 mobx-react-boilerplate,clone 下来以后直接:
npm install npm start
说明:package.json 中的
dependencies
字段比较陈旧了,能够本身手动更新到最新版本
打开控制台就能够看到 bundle.js 文件了:
这样,咱们就能够直接在 index.js 中粘贴咱们须要的代码,由于基于 Webpack 打包,因此示例代码是能够运行的。
上述两种方法由于都是使用同一个装饰器转换插件 babel-plugin-transform-decorators-legacy,因此装饰器语法部分转换后的代码是同样的。
好比针对 price
属性的装饰器语法:
@observable price = 0;
通过 babel 转译以后:
var _descriptor = _applyDecoratedDescriptor( _class.prototype, 'price', [_mobx.observable], { enumerable: true, initializer: function initializer() { return 0; } } )
而对于 total
方法的装饰器语法:
@computed get total() { return this.price * this.amount; }
通过 babel 转译以后则为:
_applyDecoratedDescriptor( _class.prototype, 'total', [_mobx.computed], Object.getOwnPropertyDescriptor(_class.prototype, 'total'), _class.prototype );
能够看到关键是使用了 _applyDecoratedDescriptor
方法。接下来咱们着重分析这个方法。
_applyDecoratedDescriptor
方法该函数签名为:
function _applyDecoratedDescriptor( target, property, decorators, descriptor, context )
具体的用法,以 price
属性为例,咱们能够获取对应的实参:
_class.prototype
,即 OrderLine.prototype
"price"
[_mobx.observable]
(不一样的修饰符装饰器是不同的,好比使用 @computed
修饰的 total
方法,就是 [_mobx.computed]
),是长度为 1 的数组,具体的 observable
方法将在下一篇文章详细讲,就是 createObservable price
)会有 initializer
属性,而方法成员(好比 total
) 则不会有这个属性,用这个来区分这两种不一样属性描述符。{ enumerable: true, initializer: function initializer() { return 0; } }
null
,对方法属性则是 _class.prototype
;看完函数签名,咱们继续看函数内容:
这几行代码没啥难度,就是咱们熟悉的 属性描述符 相关的内容:
desc
变量就是咱们熟悉的 属性描述符。所以,该 _applyDecoratedDescriptor
的做用就是根据入参返回具体的描述符。price
),就将返回的描述符就能够传给 _initDefineProp
(至关于 Object.defineProperty
)应用到原来的属性中去了,从而起到了 装饰 做用。total
)则直接应用 Object.defineProperty
方法(当是方法成员时,desc
是没有 initializer
属性的),同时令 desc = null,从后续的应用来看并不会和 _initDefineProp
方法搭配使用对于图中标注 ③ ,咱们具体看decorators
在其中发挥的做用,典型的函数式编程手法:
decorators
是 [a, b, c],那么上面的代码至关于应用公式 a(b(c(property)))
,也就是装饰器 c
先装饰属性 property
,随后再叠加装饰器 b
的做用,最后叠加装饰器 a
。以 price
属性为例,因为只有一个装饰器(@observable),因此只应用了 [_mobx.observable]
这一个装饰器。decorator(target, property, desc)
,其函数签名和 Object.defineProperty 是如出一辙。经过图中标注 ③ 咱们能够理解,当咱们写装饰器函数函数时,函数的定义入参必须是 (target, name, descriptor)
这样的,同时该函数必需要返回属性描述符。(能够停下来去翻翻看本身写装饰器函数的那些例子)至此咱们已经掌握了 babel 转换 @
语法的精髓 —— 建立了 _applyDecoratedDescriptor
方法,从而依次应用你所定义的装饰器方法,并且也明白了自定义的装饰器方法的函数签名必须是 (target, name, descriptor)
的。
总结一下这个 babel 插件对于装饰器语法 @
所作的事情:
@
语法转换成 _applyDecoratedDescriptor
方法的应用_applyDecoratedDescriptor
方法就是一个循环应用装饰器的过程那么接下来咱们回到主题,mobx 若是不使用 babel 转译,那该如何实现相似于上述装饰器的语法呢?
很显然,MobX 不能实现(也没有必要)ast 分析将 @
语法转换掉的功能,因此只能提供 循环应用装饰器 的这方面的功能。
为达到这个目的,MobX 4.x 版本相对 3.x 等之前版本多了 decorate API 方法。
官方文档 如何(不)使用装饰器 所言,使用装饰器 @
语法等价于使用 decorate
方法,即改写成以下形式:
import { observable, computed, decorate, action } from "mobx"; class OrderLine { price = 0; amount = 1; get total() { return this.price * this.amount; } } decorate(OrderLine, { price: observable, amount: observable, total: computed, increment: action.bound })
3.x 之前的版本由于没有decorate
方法,因此是借助extendObservable
方法实现的,具体见文档 在ES五、ES6和ES.next环境下使用 MobX
咱们翻开 decorate 源码,该函数声明是:
decorate(thing, decorators)
thing
:须要被装饰的原始对象;decorators
:装饰器配置对象,是一个 key/value 形式的对象, key 是属性名,value 就是具体的装饰器函数(好比 observable
、computed
和 action.bound
这样具体的装饰器有效函数)摘出核心语句:
能够看去的确就是一个 for
循环,而后依次应用 decorator
,这刚好就是 babel 插件转换后 _applyDecoratedDescriptor
方法所作的事情,所以二者是等效的。
这样,就解答了本文开篇提出的第一个疑问。 @observable、@computer 等装饰器语法,是和直接使用 decorate 是等效等价的。
看到这里是否是以为有点儿难以想象?嗯,事实上装饰器应用的过程就这么的简单。你也能够直接将这个 decorate API 方法直接提取到本身的项目中使用,给你的项目增长新的 feature。
解答完第一个问题,咱们继续讲本文开头提出的另外一个问题:MobX 中的 enhancer
是什么概念?
Enhancer 这个概念是 MobX 本身提出的一个概念,刚接触到的用户大多数会先蒙圈一下子。
学习过 MobX 3.x 及之前版本的人可能会遇到 Modifier 这个概念,Enhancer
其实就是 Modifier
。
Modifier 在 MobX 3 以前的版本里官方有专门的 文档 解说。不过到 MobX 4.x 以后官方就删除了这篇文档。好在这个概念是内部使用的,修更名字对外部调用者没有啥影响。
Enhancer
从字面上理解是 加强器,其做用就是给原有的对象 增长额外的功能 —— 这不就是装饰器的做用么?没错,它是辅助 MobX 中的 @observable
装饰器功能的。结合装饰器,会更加容易理解这个概念。
@observable
的总体关系MobX 不是有不少种装饰器么,好比 @observable
、@compute
和 @action
,注意 Enhancer
只和 @observable
有关系,和 @compute
和 @action
是没啥关系的。这是由于 Enhancer
是为观察值(observable)服务的,和计算值(computedValue)和动做(Action)不要紧。
@observable
装饰器中真正起做用的函数就是 Enhancer ,你能够将 Enhancer 理解成 @observable
装饰器有效的那部分。能够用 "药物胶囊💊" 来理解 @observable
装饰器和 Enhancer 的关系:
@observable
装饰器就像是胶囊的外壳,内里携带的药物成分就是 Enhancer,由于真正起效果的部分是 Enhancer @observable
装饰器仅仅是起到包装、传输到指定目的地的做用。@observable
相关的代码,顶可能是不能使用装饰器功能而已。@observable
装饰器是这种状况,其余的装饰器(包括 @compute
和 @action
这样的装饰器以及本身写的装饰器)都不在此讨论范畴 在 MobX 中有 4 种 Enhancer,在 types/modifier.ts 中有定义:
不理解的话能够参考 Mobx 源码解读(三) Modifier 文章,有详细的示例解说,本文就不展开了。
接下来,咱们须要解决的是有两个问题:
@observable
装饰器语法产生联系的?@observable
装饰器语法中的?这个过程讲解起来有点儿绕。但我仍是尽量讲得明白一些吧。
返回看上面示例中:
@observable price = 0;
该装饰语法最终会换成 _mobx.observable
方法的调用。
咱们看一下 observable 源码 :
export const observable: IObservableFactory & IObservableFactories & { enhancer: IEnhancer<any> } = createObservable as any
会发现 observable
是函数,其函数内容就是 createObservable。
所以上面示例中转义后的代码至关于:
return createObservable(OrderLine.prototype, 'price', desc);
继续看这个 createObservable
大致逻辑走向,该方法依据 第二个参数是否 string 类型 而起到不一样的做用:
string
,从而会调用 deepDecorator.apply(null, arguments)
,这是咱们这篇文章要继续讲的内容。探究一下 deepDecorator
的来历:
const deepDecorator = createDecoratorForEnhancer(deepEnhancer)
经过给 createDecoratorForEnhancer 方法传入 deepEnhancer
就能够了。从这个 createDecoratorForEnhancer 方法的名字就能知道其含义,基于 enhancer 建立装饰器,是否是有点神奇,直接用 Enhancer 就能建立到对应的装饰器了!MobX 中其余 enhancer 也是基于这个函数建立相应的装饰器的:
这个过程就是 @observable
装饰器语法 和 enhancer 产生联系的地方。
继续研究 createDecoratorForEnhancer
方法就能探知 Enhancer 起做用的地方。
不过接下来的函数分解,涉及到各类闭包来回整,很容易把人绕晕。这里作了一副简单的调用顺序图:
createDecoratorForEnhancer
里面会调用 createPropDecorator
和createPropDecorator
方法执行的时候会调用 defineObservableProperty
方法,createPropDecorator
是一个闭包,因此 defineObservableProperty
能在做用域中获知 enhancer
变量defineObservableProperty
中会继续调用 new ObservableValue
建立观察值,建立的过程当中会将 enhancer
做为参数传递进去。这里就不展开讲解,看得很晕也不用在乎,有个大概了解就行。感兴趣的读者,能够挨个在源码中查找上述的函数名字,感觉他们互相调用的关系,外加再看一下 defineObservableProperty 源码就能够。
下一篇文章着重分析观察值(Observable)过程的时候,还会涉及这部分逻辑,这里咱们知道大体的结论就行:最终的 enhancer
会传递给 ObservableValue
构造函数,从而影响观察值建立过程。
具体的影响在 ObservableValue
的构造函数中就体现出来,直接影响观察值对象中的 value
属性:
this.value = enhancer(value, undefined, name)
再结合 types/modifier.ts 中有各类 Enhancer 的具体内容,就能大体了解 enhancer 是如何起到 转换数值 的做用的,再分析下去就是观察值(Observable)的内容了,由于里面涉及到 递归转换 的逻辑,因此我统一会放在下一篇文章中展开讲解。
在不用 babel 转义的状况下,mobx 经过提供decorate API 实现等价装饰器功能,原理也很简单:
(target, property, desc)
(某种意义上已经成规范了)Object.defineProperty
将更改后的属性描述符 “安装” 回原始对象归纳起来就是 循环应用装饰器方法,就是那么简单粗暴有效。
能够看一下官方针对装饰器的 免责声明
至于 Enhancer,它只影响观察值(Observable)的生成,不一样的 Enhancer 会造成不一样种类的观察值(Observable);
正是由于 Enhancer 只影响观察值(Observable),因此和它相关的装饰器只有 @observable
,与 @computed
以及 @action
等装饰器无关(不过装饰器方法的定义都大同小异,只是有效成分不同罢了)。
Enhancer 是如何和 @observable
装饰器语法产生联系的呢?答案是 @observable
转义后实际上就是调用 deepDecorator
函数,而该函数须要 deepEnhancer
做为 “原材料” 才能生成的,仍是以 药物胶囊 为例来理解,@observable
就是一个壳,起到运输包装做用,真正起做用的仍旧是里面的 Enhancer。
Enhancer 真正起做用地方,是在于通过一路的闭包转换沉淀,最终会 以参数的方式 传递给 new Observable
这个构造函数中,影响所生成的观察值。
本章所讲的内容稍微枯燥一些,也并不是是 MobX 几大核心概念(Reaction、Observable、ComputedValue),然而所讲的装饰器知识一方面是理解 @
语法,另外一方面也更好地阐述 Enhancer 的概念,这些都是为了给后续要讲的观察值(Observable)打基础。并且通过这一篇文章的讲解,你能够充分体会到装饰器的概念是如此地深刻到 MobX 体系中,已俨然成为 MobX 体系中不可分割的一部分。
下面的是个人公众号二维码图片,欢迎关注,及时获取最新技术文章。