前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >用故事解读 MobX源码(四) 装饰器 和 Enhancer

用故事解读 MobX源码(四) 装饰器 和 Enhancer

作者头像
JSCON简时空
发布2020-03-31 17:10:22
9090
发布2020-03-31 17:10:22
举报
文章被收录于专栏:JSCON简时空
  • 初衷:以系列故事的方式展现源码逻辑,尽可能以易懂的方式讲解 MobX 源码;
  • 本系列文章
  • 《【用故事解读 MobX源码(一)】 autorun》
  • 《【用故事解读 MobX源码(二)】 computed》
  • 《【用故事解读 MobX源码(三)】 shouldCompute》
  • 《【用故事解读 MobX 源码(四)】装饰器 和 Enhancer》
  • 《【用故事解读 MobX 源码(五)】 Observable》
  • 文章编排:每篇文章分成两大段,第一大段以简单的侦探系列故事的形式讲解(所涉及人物、场景都以 MobX 中的概念为原型创建),第二大段则是相对于的源码讲解。
  • 本文基于 MobX 4 源码讲解

按照步骤,这篇文章应该写 观察值(Observable)的,不过在撰写的过程中发现,如果不先搞明白装饰器和 Enhancer(对这个单词陌生的,先不要着急,继续往下看) ,直接去解释观察值(Observable)会很费劲。因为在 MobX 中是使用装饰器设计模式实现观察值的,所以说要先掌握装饰器,才能进一步去理解观察值。

所以这是一篇 “插队” 的文章,用于去理解 MobX 中的装饰器和 Enhancer 概念。

A. 本文目标

本文主要解决我个人在源码阅读中的疑惑:

  • 在官方文档 如何(不)使用装饰器 中,为什么说开启 @observable、@computer 等装饰器语法,是和直接使用 decorate 是等效的?
  • 在 MobX 源码中时常出现的 Enhancer 到底是个什么概念?它在 MobX 体系中发挥怎样的作用?它和装饰器又是怎么样的一层关系?

如果你也有这样的疑惑,不妨继续阅读本文,欢迎一起讨论。

至于 观察值(Observable),在本文中你只要掌握住 官方文档 observable 的用法就足够了,比如(示例摘自官方文档):

代码语言:javascript
复制
const person = observable({
    firstName: "Clive Staples",
    lastName: "Lewis"
});
person.firstName = "C.S.";

const temperature = observable.box(20);
temperature.set(25);

对于 observable 方法的源码解析将在下一篇中详细展开,此篇文章不会做过多的讨论。

B. 学会装饰器

1、装饰器基础知识

和其他语言(Python、Java)一样,装饰器语法是借助 @ 符号实现的,现在问题就归结到如何用 JS 去实现 @ 语法。

对于还不熟悉装饰器语法的读者,这里推荐文章 《ES7 Decorator 装饰者模式》,以钢铁侠为例,通过装备特殊的装备就能将普通人变成钢铁侠,简单概括起来就是:

通过装备特殊的装备就能将普通人变成钢铁侠

装饰器设计模式的理念就和上面那样的朴素,在不改造 托尼·史塔克(Tony Stark) 本体的前提下,通过加装 盔甲飞行器 的方式增强 Tony 的能力,从而“变成”钢铁侠。

有关装饰器使用的文章,还可以参考这两篇参考文章 探寻 ECMAScript 中的装饰器 Decorator、细说ES7 JavaScript Decorators

文章都比较早,当时写文章的作者都认为在新的 ES7 里会推出标准的 @ 语法,然而事后证明官方并没有这个意愿。我们知道目前的 ECMAScript 2015 标准,甚至到 ECMAScript 2018 标准官方都没有提供 @ 语法的支持,我们在其他文章中看到的 @ 语法都是通过 babel 插件来实现的。

上面提及的参考文章都是属于应用类型的,就是直接使用装饰器语法(即直接使用 @ 语法)来展示装饰器的实际应用,而对于如何实现 @ 语法并没有提及 —— 那就是如何用 Object.defineProperty 来实现 @ 语法。

道理大家都懂,那么到底如何才能自己动手去实现 @ 装饰器语法呢?

2、首先你要理解属性描述符(descriptor)

在 JS 中,我们借助 Object.defineProperty 方法实现装饰器设计模式,该方法签名如下:

代码语言:javascript
复制
Object.defineProperty(obj, prop, descriptor)

其中最核心的其实是 descriptor —— 属性描述符

属性描述符总共分两种:数据描述符(Data descriptor)和 访问器描述符(Accessor descriptor)。

描述符必须是两种形式之一,但不能同时是两者

比如 数据描述符

代码语言:javascript
复制
Object.getOwnPropertyDescriptor(user,'name');

// 输出
/**
{
  "value": "张三",
  "writable": true,
  "enumerable": true,
  "configurable": true
}
**/

还有 访问器描述符

代码语言:javascript
复制
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 中到底是如何实现 @ 语法的?

3、搭建装饰器的 babel 示例

在理解属性描述符的基础上,我们就可以去看看 babel 对于装饰器 @ 语法的内部实现了。

就拿 MobX 官方的示例 来讲:

代码语言:javascript
复制
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。

4、有两种方式看转换之后的代码

4.1、 方法一,使用 babel 在线工具

放到 babel 在线工具,粘贴现有的示例代码会报错,不过 babel 给出了友好的提示,因为使用到了装饰器语法,需要安装 babel-plugin-transform-decorators-legacy:

使用 babel 在线工具查看转换后的代码

我们点击左下方的 Add Plugin 按钮,在弹出的搜索框里输入关键字 decorators-legacy,选择这个插件就可以:

点击 Add Plugin 按钮

选完插件之后,代码就会成功转译:

代码转换后的效果

底下会提示 require is not defined 错误,这个错误并不影响你分析装饰器的语法,因为有 @ 符号部分都已经转换成 ES5 语法了,只是这个报错无法让这段示例代码运行起来。

这是因为 Babel 只是将最新的 ES6 语法“翻译”成各大浏览器支持比较好的 ES5 语法,但模块化写法(require语句)本身就不是 ECMAScript 的标准,而是产生了其他的模块化写法标准,例如 CommonJS,AMD,UMD。因此 Babel 转码模块化写法后在浏览器中还是无法运行,此时可以考虑放到 Webpack 这种自动化构建工具环境中,此时 Webpack 是支持模块化写法的

如果有强迫症的同学,非得想要这段代码运行起来,可以参考下述的 方法二

4.2、方法二,使用 demo 工程

官方提供了 mobx-react-boilerplate,clone 下来之后直接:

代码语言:javascript
复制
npm install
npm start

说明:package.json 中的 dependencies 字段比较陈旧了,可以自己手动更新到最新版本

打开控制台就可以看到 bundle.js 文件了:

这样,我们就可以直接在 index.js 中粘贴我们需要的代码,因为基于 Webpack 打包,所以示例代码是可以运行的。

5、分析转换之后的代码逻辑

上述两种方法因为都是使用同一个装饰器转换插件 babel-plugin-transform-decorators-legacy,所以装饰器语法部分转换后的代码是一样的

比如针对 price 属性的装饰器语法:

代码语言:javascript
复制
@observable price = 0;

经过 babel 转译之后:

代码语言:javascript
复制
var _descriptor = _applyDecoratedDescriptor(
    _class.prototype,
    'price',
    [_mobx.observable],
    {
      enumerable: true,
      initializer: function initializer() {
        return 0;
      }
    }
  )

而对于 total 方法的装饰器语法:

代码语言:javascript
复制
@computed get total() {
    return this.price * this.amount;
}

经过 babel 转译之后则为:

代码语言:javascript
复制
_applyDecoratedDescriptor(
  _class.prototype,
  'total',
  [_mobx.computed],
  Object.getOwnPropertyDescriptor(_class.prototype, 'total'),
  _class.prototype
);

关键的 _applyDecoratedDescriptor 函数

可以看到关键是使用了 _applyDecoratedDescriptor 方法。接下来我们着重分析这个方法。

6、关键是 `_applyDecoratedDescriptor` 方法

该函数签名为:

代码语言:javascript
复制
function _applyDecoratedDescriptor(
  target,
  property,
  decorators,
  descriptor,
  context
)

具体的用法,以 price 属性为例,我们可以获取对应的实参:

  • target_class.prototype ,即 OrderLine.prototype
  • property:即字符串 "price"
  • decorators:在这里是 [_mobx.observable](不同的修饰符装饰器是不一样的,比如使用 @computed 修饰的 total 方法,就是 [_mobx.computed]),是长度为 1 的数组,具体的 observable 方法将在下一篇文章详细讲,就是 createObservable
  • descriptor:即属性描述符,属性成员(比如 price)会有 initializer 属性,而方法成员(比如 total) 则不会有这个属性,用这个来区分这两种不同属性描述符。
代码语言:javascript
复制
{
  enumerable: true,
  initializer: function initializer() {
    return 0;
  }
}
  • context:就是运行上下文,一般来讲对数据属性的装饰则为 null,对方法属性则是 _class.prototype

看完函数签名,我们继续看函数内容:

_applyDecoratedDescriptor 函数内容

这几行代码没啥难度,就是我们熟悉的 属性描述符 相关的内容:

  • 图中标注 ① ,表示返回的 desc 变量就是我们熟悉的 属性描述符。因此,该 _applyDecoratedDescriptor 的作用就是根据入参返回具体的描述符。
  • 如果是属性成员(比如price),就将返回的描述符就可以传给 _initDefineProp (相当于 Object.defineProperty)应用到原来的属性中去了,从而起到了 装饰 作用。

如果是属性成员,则调用 _initDefineProp 方法

  • 图中标注 ② ,表示对于方法成员(比如 total)则直接应用 Object.defineProperty 方法(当是方法成员时,desc 是没有 initializer 属性的),同时令 desc = null,从后续的应用来看并不会和 _initDefineProp 方法搭配使用

对于图中标注 ③ ,我们具体看decorators 在其中发挥的作用,典型的函数式编程手法:

应用 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 插件对于装饰器语法 @ 所做的事情:

  1. 通过 ast 分析,将 @ 语法转换成 _applyDecoratedDescriptor 方法的应用
  2. _applyDecoratedDescriptor 方法就是一个循环应用装饰器的过程

那么接下来我们回到主题,mobx 如果不使用 babel 转译,那该如何实现类似于上述装饰器的语法呢?

7、不用装饰器语法,mobx 提供了等价写法

很显然,MobX 不能实现(也没有必要)ast 分析将 @ 语法转换掉的功能,所以只能提供 循环应用装饰器 的这方面的功能。

为达到这个目的,MobX 4.x 版本相对 3.x 等以前版本多了 decorate API 方法。

官方文档 如何(不)使用装饰器 所言,使用装饰器 @ 语法等价于使用 decorate 方法,即改写成如下形式:

代码语言:javascript
复制
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 方法实现的,具体见文档 在ES5、ES6和ES.next环境下使用 MobX

我们翻开 decorate 源码,该函数声明是:

代码语言:javascript
复制
decorate(thing, decorators)
  • thing:需要被装饰的原始对象;
  • decorators:装饰器配置对象,是一个 key/value 形式的对象, key 是属性名,value 就是具体的装饰器函数(比如 observablecomputedaction.bound 这样具体的装饰器有效函数)

摘出核心语句:

decorate 源码中的关键代码

可以看去的确就是一个 for 循环,然后依次应用 decorator,这恰好就是 babel 插件转换后 _applyDecoratedDescriptor 方法所做的事情,因此两者是等效的。

这样,就解答了本文开篇提出的第一个疑问。@observable、@computer 等装饰器语法,是和直接使用 decorate 是等效等价的。

看到这里是不是觉得有点儿不可思议?嗯,事实上装饰器应用的过程就这么的简单。你也可以直接将这个 decorate API 方法直接提取到自己的项目中使用,给你的项目增加新的 feature。

解答完第一个问题,我们继续讲本文开头提出的另一个问题:MobX 中的 enhancer 是什么概念?

C. 理解 Enhancer

1、Enhancer 概念

Enhancer 这个概念是 MobX 自己提出的一个概念,刚接触到的用户大多数会先蒙圈一会儿。

学习过 MobX 3.x 及以前版本的人可能会遇到 Modifier 这个概念,Enhancer 其实就是 Modifier

Modifier 在 MobX 3 之前的版本里官方有专门的 文档 解说。不过到 MobX 4.x 之后官方就删除了这篇文档。好在这个概念是内部使用的,修改名字对外部调用者没有啥影响。

Enhancer 从字面上理解是 增强器,其作用就是给原有的对象 增加额外的功能 —— 这不就是装饰器的作用么?没错,它是辅助 MobX 中的 @observable 装饰器功能的。结合装饰器,会更加容易理解这个概念。

2、Enhancer 和 `@observable` 的整体关系

MobX 不是有很多种装饰器么,比如 @observable@compute@action,注意 Enhancer 只和 @observable 有关系,和 @compute@action 是没啥关系的。这是因为 Enhancer 是为观察值(observable)服务的,和计算值(computedValue)和动作(Action)没关系。

@observable 装饰器中真正起作用的函数就是 Enhancer ,你可以将 Enhancer 理解成 @observable 装饰器有效的那部分。可以用 "药物胶囊?" 来理解 @observable 装饰器和 Enhancer 的关系:

形象类比 @observable 装饰器语法和 Enhancer 的关系

  • @observable 装饰器就像是胶囊的外壳,内里携带的药物成分就是 Enhancer,因为真正起效果的部分是 Enhancer
  • 平时我们所接触到的 @observable 装饰器仅仅是起到包装、传输到指定目的地的作用。
  • 从另一个角度来讲,在 mobx 代码实现中,Enhancer 是实现 Observable 观察值必不可少的一部分,没有它就实现不了观察值功能,也就构建不起 MobX 体系了;而如果缺失 @observable 相关的代码,顶多是不能使用装饰器功能而已。
  • 这里还要特别强调一下,这里特指 `@observable` 装饰器是这种情况,其他的装饰器(包括 `@compute` 和 `@action` 这样的装饰器以及自己写的装饰器)都不在此讨论范畴

在 MobX 中有 4 种 Enhancer,在 types/modifier.ts 中有定义:

  • deepEnhancer:默认的,也是最常用的,它会递归地在可观察对象的属性或可观察数组、Map 的元素上调用;
  • shallowEnhancer:不对传入的值进行转换,直接返回
  • referenceEnhancer:只转换 Object, Array, Map 本身,不对其属性(或元素)转换
  • refStructEnhancer:结构内容值发生改变的时候才进行数据更新

不理解的话可以参考 Mobx 源码解读(三) Modifier 文章,有详细的示例解说,本文就不展开了。

接下来,我们需要解决的是有两个问题:

  1. Enhancer 是如何和 @observable 装饰器语法产生联系的?
  2. Enhancer 真正起作用是在什么地方?

3、Enhancer 是如何运用到 `@observable` 装饰器语法中的?

这个过程讲解起来有点儿绕。但我还是尽可能讲得明白一些吧。

返回看上面示例中:

代码语言:javascript
复制
@observable price = 0;

该装饰语法最终会换成 _mobx.observable 方法的调用。

我们看一下 observable 源码 :

代码语言:javascript
复制
export const observable: IObservableFactory &
    IObservableFactories & {
        enhancer: IEnhancer<any>
    } = createObservable as any

会发现 observable 是函数,其函数内容就是 createObservable。

因此上面示例中转义后的代码相当于:

代码语言:javascript
复制
return createObservable(OrderLine.prototype, 'price', desc);

继续看这个 createObservable 大体逻辑走向,该方法依据 第二个参数是否 string 类型 而起到不同的作用:

createObservable方法内根据第二个参数进行不同的处理

  • 如果第二个参数不是 string 类型,会走图中所示 ① 的逻辑,相当于 转换函数,将普通属性转换成 Observable 对象;这部分逻辑我们下一篇文章会着重讲到,这里暂且略过;
  • 如果第二个参数是 string 类型 ,那么就是本文所述起到 装饰器 作用,此时方法第二个入参必须是 string,从而会调用 deepDecorator.apply(null, arguments),这是我们这篇文章要继续讲的内容。

探究一下 deepDecorator 的来历:

代码语言:javascript
复制
const deepDecorator = createDecoratorForEnhancer(deepEnhancer)

通过给 createDecoratorForEnhancer 方法传入 deepEnhancer 就可以了。从这个 createDecoratorForEnhancer 方法的名字就能知道其含义,基于 enhancer 创建装饰器,是不是有点神奇,直接用 Enhancer 就能创建到对应的装饰器了!MobX 中其他 enhancer 也是基于这个函数创建相应的装饰器的:

直接用 Enhancer 就能创建到对应的装饰器了

这个过程就是 @observable 装饰器语法 和 enhancer 产生联系的地方。

4、Enhancer 真正起作用是在什么地方?

继续研究 createDecoratorForEnhancer 方法就能探知 Enhancer 起作用的地方。

不过接下来的函数分解,涉及到各种闭包来回整,很容易把人绕晕。这里做了一副简单的调用顺序图:

用一副简单的调用顺序图来理解 createDecoratorForEnhancer 源码

  • createDecoratorForEnhancer 里面会调用 createPropDecorator
  • createPropDecorator 方法执行的时候会调用 defineObservableProperty 方法,createPropDecorator 是一个闭包,所以 defineObservableProperty 能在作用域中获知 enhancer 变量
  • defineObservableProperty 中会继续调用 new ObservableValue 创建观察值,创建的过程中会将 enhancer 作为参数传递进去。

这里就不展开讲解,看得很晕也不用在意,有个大概了解就行。感兴趣的读者,可以挨个在源码中查找上述的函数名字,感受他们互相调用的关系,外加再看一下 defineObservableProperty 源码就可以。

下一篇文章着重分析观察值(Observable)过程的时候,还会涉及这部分逻辑,这里我们知道大致的结论就行:最终的 enhancer 会传递给 ObservableValue 构造函数,从而影响观察值创建过程

具体的影响在 ObservableValue 的构造函数中就体现出来,直接影响观察值对象中的 value 属性:

代码语言:javascript
复制
this.value = enhancer(value, undefined, name)

在创建 observable 时发挥作用

再结合 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 体系中不可分割的一部分。

—END—

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JSCON简时空 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • A. 本文目标
  • B. 学会装饰器
    • 1、装饰器基础知识
      • 2、首先你要理解属性描述符(descriptor)
        • 3、搭建装饰器的 babel 示例
          • 4、有两种方式看转换之后的代码
            • 4.1、 方法一,使用 babel 在线工具
            • 4.2、方法二,使用 demo 工程
          • 5、分析转换之后的代码逻辑
            • 6、关键是 `_applyDecoratedDescriptor` 方法
              • 7、不用装饰器语法,mobx 提供了等价写法
              • C. 理解 Enhancer
                • 1、Enhancer 概念
                  • 2、Enhancer 和 `@observable` 的整体关系
                    • 3、Enhancer 是如何运用到 `@observable` 装饰器语法中的?
                      • 4、Enhancer 真正起作用是在什么地方?
                      • 本文小结
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档