Decorators 低侵入性探索

当你们都再聊要不要学习框架的时候,笔者却还在学规范,当标题党。本文的一切,源于网络,感恩开源的世界...javascript

虽然本文的初衷是讲 ES7 中的装饰器,但笔者更喜欢在探索的过程当中加深对前端基础知识的理解。本着一颗刨根问底儿的心,分享内容会尽量多地将一些关联知识串联起来说解。html

乍一看可能会有点乱,但倒是笔者学习一个新知识的完整路径。 一种带着关键词去学习的方法,比较笨,读者选读便可,取精华去糟粕。前端

另外,这个仓库 是专门用来记录 Decorators 低侵入性探索 收获的知识。后续可能会结合 mobx 源码、以及在 React 中实际应用场景来深刻。vue

前端知识广度一望无际,深度深不可测,笔者记性很差,相似的仓库有:java

概览

Decorators 属于 ES7, 目前处于提案阶段,可经过 babelTS 编译使用。git

本文属于探索型,主要分为三部分:github

  • Decorators 基础知识npm

  • Babel 与 TypeScript 支持json

  • 常见应用场景数组

基础知识

装饰器 (Decorators) 让你能够在设计时对类和类的属性进行“注解”和修改。

Decorators 通常接受三个参数:

  • 目标对象 target

  • 属性名称 key

  • 描述对象 descriptor

可选地返回一个描述对象来安装到目标对象上,其的函数签名为

function(target, key?, descriptor?)

Object.defineProperty

Decorators 的本质是利用了 ES5 的 Object.defineProperty 方法,这个方法着实改变了不少,好比 vue 响应式数据的实现方法,固然还有更为迷人 proxy,是否是发现,不少框架背后的靠山都离不开这些底层规范的支持。

下面来简单了解下这个方法:

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

Object.defineProperty(obj, prop, descriptor)

  • obj 要在其上定义属性的对象。

  • prop 要定义或修改的属性的名称。

  • descriptor 将被定义或修改的属性描述符。

  • 返回值 被传递给函数的对象。

其中 descriptor 可经过 Object.getOwnPropertyDescriptor() 方法得到。

Object.getOwnPropertyDescriptor

Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不须要从原型链上进行查找的属性)

  • obj 须要查找的目标对象

  • prop 目标对象内属性名称(String 类型)

  • 返回值 若是指定的属性存在于对象上,则返回其属性描述符对象(property descriptor),不然返回 undefined

Descriptor

一个属性描述符是一个记录,由下面属性当中的某些组成的:

  • value 该属性的值(仅针对数据属性描述符有效)

  • writable 当且仅当属性的值能够被改变时为 true。(仅针对数据属性描述有效)

  • configurable 当且仅当指定对象的属性描述能够被改变或者属性可被删除时,为 true

  • enumerable 当且仅当指定对象的属性能够被枚举出时,为 true

  • get 获取该属性的访问器函数(getter)。若是没有访问器, 该值为 undefined。(仅针对包含访问器或设置器的属性描述有效)

  • set 获取该属性的设置器函数(setter)。 若是没有设置器, 该值为 undefined。(仅针对包含访问器或设置器的属性描述有效)

各式的装饰器通常都是基于修改上述属性来实现,好比 writable可用于设置 @readonly。更多的功能,可参考 lodash-decorator

基础知识小结

如今咱们对 Decorators 方法 function(target, key?, descriptor?) 混了个脸熟,同时知道了Object.definePropertyDescriptor 与 Decorators 的联系。

可是,目前浏览器对 Es7 这一特性支持 并不友好。Decorators 目前还只是语法糖,尝鲜可经过 babel 、TypeScript。

接下来就来了解这一部分的内容。

babel 与 Decorators

不少构建工具都离不开 babel,好比笔者用于快速跑 demo 的 parcel。虽然不少时候咱们并不须要关心这些构建后的代码,但笔者建议有时间仍是多了解下,毕竟前端打包后出现的 bug 仍是很常见的。

回到装饰器,现阶段官方说有 2 种装饰器,但从实际使用上可分为 4 种,分别是:

  • 类装饰器” 做用于 class

  • 属性装饰器” 做用于属性上的,这须要配合另外一个的类属性语法提案,或者做用于对象字面量。

  • 方法装饰器” 做用于方法上。

  • 访问器装饰器” 做用于 gettersetter 上的。

下面咱们经过 babel 命令行,来感觉一下各装饰器:

babel 配置

先简单介绍下 babel 的用法:

  1. 全局安装 babel
npm i -g babel
复制代码
  1. 配置 .babelrc
{
  "presets": [["es2015", { "modules": false }]],
  "plugins": ["transform-decorators-legacy", "transform-class-properties"],
  "env": {
    "development": {
      "plugins": ["transform-es2015-modules-commonjs"]
    }
  }
}
复制代码
  1. package.json 配置 npm script
{
  "babel": "babel ./demo/demo.js -w --out-dir dist"
}
复制代码

该命令的意思是:监听 demo 目录下 demo.js 文件,并将编译结果输出到 dist 目录

下面列出各装饰器在 babel 编译后对应的输出结果。

“类装饰器”

从编译后的结果能够看到,autobind 做为装饰器只接受了一个参数,也就是类自己(构造函数)。

class MyClass = {}
MyClass = autobind(MyClass) || MyClass
复制代码

“方法装饰器”

bebel 对于方法装饰器的处理会比较特别,下面看下核心处理:

var _class;

// 一、首先,初始化一个 class
var initClass = (_class = (function() {
  // ... 类定义
})());

// 二、经过 `_applyDecoratedDescriptor` 方法使用传入的装饰器对 `_class.prototype` 中的方法进行装饰处理。
var Decorator = _applyDecoratedDescriptor(
  _class.prototype,
  'getName',
  [autobind],
  Object.getOwnPropertyDescriptor(_class.prototype, 'getName'),
  _class.prototype
);

// 三、利用逗号操做符的做用,返回装饰完的 `_class`
var MyClass = (initClass, Decorator, _class);
复制代码

后续会对 _applyDecoratedDescriptor 方法进一步讲解。

逗号操做符 对它的每一个操做数求值(从左到右),并返回最后一个操做数的值。

“访问器装饰器”

“访问器装饰器” 的处理方式与 “方法装饰器”相似。

“属性装饰器”

区别在于传入的第三个参数 Descriptor 并非由 Object.getOwnPropertyDescriptor(_class.prototype, 'getName') 返回的,而且多了一个 Descriptor 上并不存在的 initializer 属性供 _applyDecoratedDescriptor 方法使用。

_applyDecoratedDescriptor(
  _class.prototype,
  'getName',
  [autobind],
  // Object.getOwnPropertyDescriptor(_class.prototype, 'getName'),
  {
    enumerable: true,
    initializer: function initializer() {
      return function() {};
    }
  }
))
复制代码

接下来就让咱们来看一下 _applyDecoratedDescriptor 都作了哪些事

_applyDecoratedDescriptor

_applyDecoratedDescriptor 实际上是对 decorator 的一个封装,用于处理多种状况。其接受的参数跟 decorator 大致一致。

  • target 目标对象

  • property 属性名称

  • descriptor 属性描述对象

  • decorators 装饰器函数 (数组,表示可传入多个装饰器)

  • context 上下文

  • 返回值 属性描述对象

function _applyDecoratedDescriptor( target, property, decorators, descriptor, context ) {
  // 一、经过传入参数 `descriptor` 初始化最终导出的 `属性描述对象`
  var desc = {};
  Object['ke' + 'ys'](descriptor).forEach(function(key) {
    desc[key] = descriptor[key];
  });
  desc.enumerable = !!desc.enumerable;
  desc.configurable = !!desc.configurable;

  // 二、存在 `value` 或者 class 初始化属性 则将 `writable` 设置为 `true`
  if ('value' in desc || desc.initializer) {
    desc.writable = true;
  }

  // 三、处理传入的 decorator 函数
  // 其中 `reverse` 保证了,当同一个方法有多个装饰器,会由内向外执行。
  desc = decorators
    .slice()
    .reverse()
    .reduce(function(desc, decorator) {
      return decorator(target, property, desc) || desc;
    }, desc);

  // 看 babel 编译后的代码,当 `initializer` 不为 `undefined` 时,并不会传入 `context`
  // 笔者看不懂! ??? 这是一个永远不会执行的逻辑... 难道改走 `_initDefineProp` 逻辑了?
  if (context && desc.initializer !== void 0) {
    desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
    desc.initializer = undefined;
  }

  // 4. 使用 Object.defineProperty 对 `target` 对象的 `property` 属性赋值为 `desc`
  if (desc.initializer === void 0) {
    Object['define' + 'Property'](target, property, desc);
    desc = null;
  }

  return desc;
}
复制代码

void 运算符 对给定的表达式进行求值,而后返回 undefined

如今咱们对 Descorators 有了大体的了解,接下来看下 Descorators 基于 babel 编译下的装饰器

自动绑定 this

咱们先来看一个关于 this 的问题

this 的指向问题

class Person {
  getPerson() {
    return this;
  }
}

let person = new Person();
const { getPerson } = person;

getPerson() === person; // false
person.getPerson() === person; // true
复制代码

这段代码中,getPersonperson.getPerson 指向同一个函数且返回 this ,但它们的执行结果却不同。

this 指的是函数运行时所在的环境:

  • getPerson() 运行在全局环境,因此 this 指向全局环境

  • person.getPerson 运行在 person 环境,因此 this 指向 person

关于 this 的原理能够参考 这篇

在本例中,getPerson() 是一个函数,JavaScript 引擎会将函数单独保存在内存中,而后再将函数的地址赋值给 getPerson 属性的 value 属性 (descriptor)

因为函数单独存在于内存中,因此它能够在不一样的环境 (上下文) 执行。

来看个例子:

// 注意,这里都是用 var 声明变量

var name = 'globalName';

var fn = function() {
  console.log(this.name);
  return this.name;
};

var person = {
  getPerson: fn,
  name: 'personName'
};

// 单独执行
var ref = person.getPerson;
ref();

// or
fn();

// person 环境指执行
person.getPerson();
复制代码

函数能够在不一样的运行环境 (context),因此须要一种机制,可以在函数体内部得到当前的运行环境。

这里 this 的设计目的就是在函数体内部,指代函数当前的运行环境。

例子中,fn()ref() 的运行环境都是 全局运行环境person.getPerson() 的运行环境是 person,所以获得了不一样的 this

解决 this 指向的方法有不少种,好比函数的原型方法

经过上面学习到的知识,接着来说解 Decorator 中如何实现 autobind 给函数或类自动绑定 this

autobind 实现逻辑

1、 首先来看下 如何给类的方法自动绑定 this

  1. 开始前,先来运行下面这段代码:
var obj = {
  fn: function() {
    console.log('执行时的', this);
  }
};

var fn = Object.getOwnPropertyDescriptor(obj, 'fn').value;

Object.defineProperty(obj, 'fn', {
  get() {
    console.log('get 访问器里的', this);
    return fn;
  }
});

var fn = obj.fn;
fn();
obj.fn();
复制代码

  1. 能够获得的一个结论:get(){} 访问器属性里面的 this 始终指向 obj 这个对象。

  2. 若是简化逻辑,也就是不考虑其余特殊状况下,autobindMethod 应该是这样的:

function autobindMethod(target, key, { value: fn, configurable, enumerable }) {
  return {
    configurable,
    enumerable,
    get() {
      const boundFn = fn.bind(this);
      defineProperty(this, key, {
        configurable: true,
        writable: true,
        enumerable: false,
        value: boundFn
      });
      return boundFn;
    },
    set: createDefaultSetter(key)
  };
}
复制代码

bind() 方法建立一个新的函数, 当这个新函数被调用时 this 键值为其提供的值,其参数列表前几项值为建立时指定的参数序列。

有了 autobind 这个装饰器,getName 方法的 this 就始终指向实例对象自己了。

class TestGet {
  @autobind
  getName() {
    console.log(this);
  }
}
复制代码

2、接着来看下类的 autobind 实现

对类绑定 this 其实就是为了批量给类的实例方法绑定 this 因此只要获取全部实例方法,再调用 autobindMethod 便可。

function autobindClass(klass) {
  const descs = getOwnPropertyDescriptors(klass.prototype);
  const keys = getOwnKeys(descs);

  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i];
    const desc = descs[key];

    if (typeof desc.value !== 'function' || key === 'constructor') {
      continue;
    }

    defineProperty(
      klass.prototype,
      key,
      autobindMethod(klass.prototype, key, desc)
    );
  }
}
复制代码

以上实现考虑的是 Babel 编译后的文件,除了 Babel ,TypeScript 也支持编译 Decorators。

所以就须要一个更为通用的 Decorators 包装函数,接下来让咱们一块儿实现它。

TypeScript 与 Decorators

先来一块儿看下 TypeScript 编译后的结果。

从上图能够看出,TypeScript 对 Decorator 编译的结果跟 Babel 略微不一样,TypeScript 对属性和方法没有过多的处理,惟一的区别可能就是在对类的处理上,传入的 target 为类自己,而不是 Prototype

通用 Decorator

不管是用什么编译器生成的代码,最终参数仍是离不开 target, name, descriptor。另外,不管怎么包装,最终也是为了提供一个可以新增或者修改 descriptor 某个属性的函数,只要是对属性的修改,就必然离不开 Object.defineProperty

有时候,咱们难以读懂某段代码,可能只是由于没有进入这段代码的真实上下文(应用场景)。若是是按需求来开发某个 Decorator,事情就会变得简单。

通用 Decorator,意味着将要用于生成具备共有特征且用于不一样场景的装饰器,一般最容易让人想到就是工厂模式。

咱们来看下 lodash-decorators 中的实现:

export class InternalDecoratorFactory {
  createDecorator(config: DecoratorConfig): GenericDecorator {
    // 基础装饰器
  }

  createInstanceDecorator(config: DecoratorConfig): GenericDecorator {
    // 生成用于实例的装饰器
  }

  private _isApplicable(
    context: InstanceChainContext,
    config: DecoratorConfig
  ): boolean {
    // 是否可调用
  }

  private _resolveDescriptor(
    target: Object,
    name: string,
    descriptor?: PropertyDescriptor
  ): PropertyDescriptor {
    // 获取 Descriptor 的通用方法。
  }
}
复制代码

这里用 TypeScript 的好处在于,类自己具有某种结构。也就是可供类型描述使用。另外,在看源码过程当中,TypeScript 的类型有助于快速理解做者意图。

好比单看上面代码,咱们就能够知道 createDecoratorcreateInstanceDecorator 都接收类型为 DecoratorConfig 的参数,以及返回都是通用的 Decorator GenericDecorator

那咱们先来看下:

export interface DecoratorConfigOptions {
  bound?: boolean;
  setter?: boolean;
  getter?: boolean;
  property?: boolean;
  method?: boolean;
  optionalParams?: boolean; // 是否使用自定义参数
}

export class DecoratorConfig {
  constructor( public readonly execute: Function, // 处理函数,如传入 debounce 函数 public readonly applicator: Applicator, // 根据处理函数不一样,选用不一样的函数调用程序。 public readonly options: DecoratorConfigOptions = {} ) {}
}
复制代码

关键的参数有:

  • execute 装饰函数的核心处理函数。
  • applicator 主要做用是用于配置参数及函数的调用。
  • options 额外的配置选项,如是不是属性,是不是方法,是否使用自定义参数等。

这里的 Applicator 属于函数调用中公共部分的抽离:

export interface ApplicateOptions {
  config: DecoratorConfig;
  target: any;
  value: any;
  args: any[];
  instance?: Object;
}

export abstract class Applicator {
  abstract apply(options: ApplicateOptions): any;
}
复制代码

一个通用的 Decorator 的核心部分差很少就这些了,但因为笔者实际应用 Decorators 的地方很少,对于 lodash-decorators 源码中为何有 createDecoratorcreateInstanceDecorator 两种生成方法,以及为何要引入 weekMap 的缘由,一时也给不了很是准确的答案。createInstanceDecorator 也许是出于原型链考虑?由于实例,才能访问原型链继承后获得的方法,之后有机会再单独深刻。

但愿有这方面研究的读者能够不吝赐教,笔者不胜感激

常见应用场景

结合 lodash,关注点分离了。实现各类 decorators 在代码实现上就变得很是简单。好比,前端可能会常常用到的函数节流函数防抖delay

import debounce = require('lodash/debounce');
import { PreValueApplicator } from './applicators';

const decorator = DecoratorFactory.createInstanceDecorator(
  new DecoratorConfig(debounce, new PreValueApplicator(), { setter: true })
);

export function Debounce( wait?: number, options?: DebounceOptions ): LodashDecorator {
  return decorator(wait, options);
}
复制代码

经过调用 DecoratorFactory 生成通用的 decorator,实现各类装饰器功能就只须要像上面同样组织代码便可。

另外像 Mixin 这种看似组合优于继承的用法是一种对类的装饰,能够这么去实现:

import assign = require('lodash/assign');

export function Mixin(...srcs: Object[]): ClassDecorator {
  return ((target: Function) => { assign(target.prototype, ...srcs); return target; }) as any; } 复制代码

更多的功能,笔者就再也不过多赘述。再讲就变成 lodash 源码解析了。有心的读者能够去触类旁通了,或者直接看 lodash-decorators 源码。毕竟我也是看它们源码来学习的。

总结

这么草率的结束,也许意味着还有更多学习空间。

Decorators 涉及的知识并不难,关键在于如何巧妙运用。初期没经验,能够学习笔者看些周边库,好比 lodash-decorators。所谓的低侵入性,也只是视觉感官上的,不过确实多少能提升代码的可读性。

最后,前端路上,多用 【闻道有前后,术业有专攻】安慰本身,学习永无止境。 感谢阅读,愿君多采撷!

参考

相关文章
相关标签/搜索