【用故事解读 MobX 源码(四)】装潢器 和 Enhancer

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

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

根据步骤,这篇文章应当写 视察值(Observable)的,不过在撰写的历程当中发明,如果不先搞邃晓装潢器和 Enhancer(对这个单词生疏的,先不要焦急,继承往下看) ,直接去诠释视察值(Observable)会很费力。因为在 MobX 中是运用装潢器设想形式完成视察值的,所以说要先控制装潢器,才进一步去明白视察值。

所以这是一篇 “插队” 的文章,用于去明白 MobX 中的装潢器和 Enhancer 观点。

A. 本文目标

本文重要处理我个人在源码浏览中的迷惑:

  • 在官方文档 如何(不)运用装潢器 中,为何说开启 @observable@computer 等装潢器语法,是和直接运用 decorate 是等效的?
  • 在 MobX 源码中经常涌现的 Enhancer 究竟是个什么观点?它在 MobX 系统中发挥如何的作用?它和装潢器又是如何的一层关联?

如果你也有如许的迷惑,无妨继承浏览本文,迎接一同议论。

至于 视察值(Observable),在本文中你只需控制住 官方文档 observable 的用法就足够了,比方(示例摘自官方文档):

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 装潢者形式》,以钢铁侠为例,经由历程设备特别的设备就可以将一般人变成钢铁侠,简朴归纳综合起来就是:

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

装潢器设想形式的理念就和上面那样的质朴,在不革新 托尼·史塔克(Tony Stark) 本体的前提下,经由历程加装 盔甲飞行器 的体式格局加强 Tony 的才能,从而“变成”钢铁侠。

有关装潢器运用的文章,还能够参考这两篇参考文章 探访 ECMAScript 中的装潢器 Decorator细说ES7 JavaScript Decorators

文章都比较早,当时写文章的作者都以为在新的 ES7 里会推出范例的 @ 语法,但是预先证实官方并没有这个志愿。我们晓得现在的 ECMAScript 2015 范例,以至到 ECMAScript 2018 范例官方都没有供应 @ 语法的支撑,我们在其他文章中看到的 @ 语法都是经由历程 babel 插件来完成的。

上面说起的参考文章都是属于运用范例的,就是直接运用装潢器语法(即直接运用 @ 语法)来展现装潢器的现实运用,而关于如何完成 @ 语法并没有说起 —— 那就是如何用 Object.defineProperty 来完成 @ 语法。

道理人人都懂,那末究竟如何才自身着手去完成 @ 装潢器语法呢?

2、起首你要明白属性描述符(descriptor)

在 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 中究竟是如何完成 @ 语法的?

3、搭建装潢器的 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

4、有两种体式格局看转换以后的代码

4.1、 要领一,运用 babel 在线东西

放到 babel 在线东西,粘贴现有的示例代码会报错,不过 babel 给出了友爱的提醒,因为运用到了装潢器语法,需要装置 babel-plugin-transform-decorators-legacy

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

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

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

选完插件以后,代码就会胜利转译:

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

底下会提醒 require is not defined 毛病,这个毛病并不影响你剖析装潢器的语法,因为有 @ 标记部份都已转换成 ES5 语法了,只是这个报错没法让这段示例代码运转起来。

这是因为 Babel 只是将最新的 ES6 语法“翻译”成各大浏览器支撑比较好的 ES5 语法,但模块化写法(
require语句)自身就不是 ECMAScript 的范例,而是发生了其他的模块化写法范例,比方 CommonJS,AMD,UMD。因而 Babel 转码模块化写法后在浏览器中照样没法运转,此时能够斟酌放到 Webpack 这类自动化构建东西环境中,此时 Webpack 是支撑模块化写法的

如果有强迫症的同砚,非得想要这段代码运转起来,能够参考下述的 要领二

4.2、要领二,运用 demo 工程

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

npm install
npm start

申明:package.json 中的
dependencies 字段比较陈腐了,能够自身手动更新到最新版本

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

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

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

5、剖析转换以后的代码逻辑

上述两种要领因为都是运用同一个装潢器转换插件 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
);

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

能够看到关键是运用了 _applyDecoratedDescriptor 要领。接下来我们偏重剖析这个要领。

6、关键是 _applyDecoratedDescriptor 要领

该函数署名为:

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) 则不会有这个属性,用这个来辨别这两种差别属性描述符。
{
  enumerable: true,
  initializer: function initializer() {
    return 0;
  }
}
  • context:就是运转上下文,平常来说对数据属性的装潢则为 null,对要领属性则是 _class.prototype

看完函数署名,我们继承看函数内容:

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

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

  • 图中标注 ① ,示意返回的 desc 变量就是我们熟习的 属性描述符。因而,该 _applyDecoratedDescriptor 的作用就是根据入参返回详细的描述符。
  • 如果是属性成员(比方price),就将返回的描述符就可以够传给 _initDefineProp (相当于 Object.defineProperty)运用到本来的属性中去了,从而起到了 装潢 作用。

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

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

关于图中标注 ③ ,我们详细看decorators 在其中发挥的作用,典范的函数式编程手段:

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

  • 起首团体上来看,是一个轮回语句。如果我们传入的 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 要领,即改写成以下情势:


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 源码,该函数声明是:

decorate(thing, decorators)
  • thing:需要被装潢的原始对象;
  • decorators:装潢器设置对象,是一个 key/value 情势的对象, key 是属性名,value 就是详细的装潢器函数(比方 observablecomputedaction.bound 如许详细的装潢器有用函数)

摘出中心语句:

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

能够看去确实就是一个 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 的关联:

《【用故事解读 MobX 源码(四)】装潢器 和 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 装潢器语法中的?

这个历程说明注解起来有点儿绕。但我照样尽量讲得邃晓一些吧。

返回看上面示例中:

@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 范例 而起到差别的作用:

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

  • 如果第二个参数不是 string 范例,会走图中所示 ① 的逻辑,相当于 转换函数,将一般属性转换成 Observable 对象;这部份逻辑我们下一篇文章会偏重讲到,这里临时略过;
  • 如果第二个参数是 string 范例 ,那末就是本文所述起到 装潢器 作用,此时要领第二个入参必需是 string,从而会挪用 deepDecorator.apply(null, arguments),这是我们这篇文章要继承讲的内容。

探讨一下 deepDecorator 的来源:

const deepDecorator = createDecoratorForEnhancer(deepEnhancer)

经由历程给 createDecoratorForEnhancer 要领传入 deepEnhancer 就可以够了。从这个 createDecoratorForEnhancer 要领的名字就可以晓得其寄义,基于 enhancer 建立装潢器,是不是是有点奇异,直接用 Enhancer 就可以建立到对应的装潢器了!MobX 中其他 enhancer 也是基于这个函数建立响应的装潢器的:

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

这个历程就是 @observable 装潢器语法 和 enhancer 发生联络的处所。

4、Enhancer 真正起作用是在什么处所?

继承研讨 createDecoratorForEnhancer 要领就可以探知 Enhancer 起作用的处所。

不过接下来的函数剖析,触及到种种闭包往返整,很轻易把人绕晕。这里做了一副简朴的挪用递次图:

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

  • createDecoratorForEnhancer 内里会挪用 createPropDecorator
  • createPropDecorator 要领实行的时刻会挪用 defineObservableProperty 要领,createPropDecorator 是一个闭包,所以 defineObservableProperty 能在作用域中获知 enhancer 变量
  • defineObservableProperty 中会继承挪用 new ObservableValue 建立视察值,建立的历程当中会将 enhancer 作为参数通报进去。

这里就不睁开说明注解,看得很晕也不必在乎,有个也许相识就行。感兴趣的读者,能够挨个在源码中查找上述的函数名字,感觉他们相互挪用的关联,外加再看一下 defineObservableProperty 源码就可以够。

下一篇文章偏重剖析视察值(Observable)历程的时刻,还会触及这部份逻辑,这里我们晓得大抵的结论就行:终究的 enhancer 会通报给 ObservableValue 组织函数,从而影响视察值建立历程

详细的影响在 ObservableValue 的组织函数中就表现出来,直接影响视察值对象中的 value 属性:

this.value = enhancer(value, undefined, name)

《【用故事解读 MobX 源码(四)】装潢器 和 Enhancer》

再连系 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 系统中不可分割的一部份。

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

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