Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 系列之我们来聊聊装饰器 #109

Open
mqyqingfeng opened this issue Nov 14, 2018 · 14 comments
Open

ES6 系列之我们来聊聊装饰器 #109

mqyqingfeng opened this issue Nov 14, 2018 · 14 comments

Comments

@mqyqingfeng
Copy link
Owner

Decorator

装饰器主要用于:

  1. 装饰类
  2. 装饰方法或属性

装饰类

@annotation
class MyClass { }

function annotation(target) {
   target.annotated = true;
}

装饰方法或属性

class MyClass {
  @readonly
  method() { }
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

Babel

安装编译

我们可以在 Babel 官网的 Try it out,查看 Babel 编译后的代码。

不过我们也可以选择本地编译:

npm init

npm install --save-dev @babel/core @babel/cli

npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties

新建 .babelrc 文件

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", {"loose": true}]
  ]
}

再编译指定的文件

babel decorator.js --out-file decorator-compiled.js

装饰类的编译

编译前:

@annotation
class MyClass { }

function annotation(target) {
   target.annotated = true;
}

编译后:

var _class;

let MyClass = annotation(_class = class MyClass {}) || _class;

function annotation(target) {
  target.annotated = true;
}

我们可以看到对于类的装饰,其原理就是:

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

装饰方法的编译

编译前:

class MyClass {
  @unenumerable
  @readonly
  method() { }
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

function unenumerable(target, name, descriptor) {
  descriptor.enumerable = false;
  return descriptor;
}

编译后:

var _class;

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context ) {
	/**
	 * 第一部分
	 * 拷贝属性
	 */
	var desc = {};
	Object["ke" + "ys"](descriptor).forEach(function(key) {
		desc[key] = descriptor[key];
	});
	desc.enumerable = !!desc.enumerable;
	desc.configurable = !!desc.configurable;

	if ("value" in desc || desc.initializer) {
		desc.writable = true;
	}

	/**
	 * 第二部分
	 * 应用多个 decorators
	 */
	desc = decorators
		.slice()
		.reverse()
		.reduce(function(desc, decorator) {
			return decorator(target, property, desc) || desc;
		}, desc);

	/**
	 * 第三部分
	 * 设置要 decorators 的属性
	 */
	if (context && desc.initializer !== void 0) {
		desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
		desc.initializer = undefined;
	}

	if (desc.initializer === void 0) {
		Object["define" + "Property"](target, property, desc);
		desc = null;
	}

	return desc;
}

let MyClass = ((_class = class MyClass {
	method() {}
}),
_applyDecoratedDescriptor(
	_class.prototype,
	"method",
	[readonly],
	Object.getOwnPropertyDescriptor(_class.prototype, "method"),
	_class.prototype
),
_class);

function readonly(target, name, descriptor) {
	descriptor.writable = false;
	return descriptor;
}

装饰方法的编译源码解析

我们可以看到 Babel 构建了一个 _applyDecoratedDescriptor 函数,用于给方法装饰。

Object.getOwnPropertyDescriptor()

在传入参数的时候,我们使用了一个 Object.getOwnPropertyDescriptor() 方法,我们来看下这个方法:

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

顺便注意这是一个 ES5 的方法。

举个例子:

const foo = { value: 1 };
const bar = Object.getOwnPropertyDescriptor(foo, "value");
// bar {
//   value: 1,
//   writable: true
//   enumerable: true,
//   configurable: true,
// }

const foo = { get value() { return 1; } };
const bar = Object.getOwnPropertyDescriptor(foo, "value");
// bar {
//   get: /*the getter function*/,
//   set: undefined
//   enumerable: true,
//   configurable: true,
// }

第一部分源码解析

在 _applyDecoratedDescriptor 函数内部,我们首先将 Object.getOwnPropertyDescriptor() 返回的属性描述符对象做了一份拷贝:

// 拷贝一份 descriptor
var desc = {};
Object["ke" + "ys"](descriptor).forEach(function(key) {
	desc[key] = descriptor[key];
});
desc.enumerable = !!desc.enumerable;
desc.configurable = !!desc.configurable;

// 如果没有 value 属性或者没有 initializer 属性,表明是 getter 和 setter
if ("value" in desc || desc.initializer) {
	desc.writable = true;
}

那么 initializer 属性是什么呢?Object.getOwnPropertyDescriptor() 返回的对象并不具有这个属性呀,确实,这是 Babel 的 Class 为了与 decorator 配合而产生的一个属性,比如说对于下面这种代码:

class MyClass {
  @readonly
  born = Date.now();
}

function readonly(target, name, descriptor) {
  descriptor.writable = false;
  return descriptor;
}

var foo = new MyClass();
console.log(foo.born);

Babel 就会编译为:

// ...
(_descriptor = _applyDecoratedDescriptor(_class.prototype, "born", [readonly], {
	configurable: true,
	enumerable: true,
	writable: true,
	initializer: function() {
		return Date.now();
	}
}))
// ...

此时传入 _applyDecoratedDescriptor 函数的 descriptor 就具有 initializer 属性。

第二部分源码解析

接下是应用多个 decorators:

/**
 * 第二部分
 * @type {[type]}
 */
desc = decorators
	.slice()
	.reverse()
	.reduce(function(desc, decorator) {
		return decorator(target, property, desc) || desc;
	}, desc);

对于一个方法应用了多个 decorator,比如:

class MyClass {
  @unenumerable
  @readonly
  method() { }
}

Babel 会编译为:

_applyDecoratedDescriptor(
	_class.prototype,
	"method",
	[unenumerable, readonly],
	Object.getOwnPropertyDescriptor(_class.prototype, "method"),
	_class.prototype
)

在第二部分的源码中,执行了 reverse() 和 reduce() 操作,由此我们也可以发现,如果同一个方法有多个装饰器,会由内向外执行。

第三部分源码解析

/**
 * 第三部分
 * 设置要 decorators 的属性
 */
if (context && desc.initializer !== void 0) {
	desc.value = desc.initializer ? desc.initializer.call(context) : void 0;
	desc.initializer = undefined;
}

if (desc.initializer === void 0) {
	Object["define" + "Property"](target, property, desc);
	desc = null;
}

return desc;

如果 desc 有 initializer 属性,意味着当装饰的是类的属性时,会将 value 的值设置为:

desc.initializer.call(context)

而 context 的值为 _class.prototype,之所以要 call(context),这也很好理解,因为有可能

class MyClass {
  @readonly
  value = this.getNum() + 1;

  getNum() {
    return 1;
  }
}

最后无论是装饰方法还是属性,都会执行:

Object["define" + "Property"](target, property, desc);

由此可见,装饰方法本质上还是使用 Object.defineProperty() 来实现的。

应用

1.log

为一个方法添加 log 函数,检查输入的参数:

class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

function log(target, name, descriptor) {
  var oldValue = descriptor.value;

  descriptor.value = function(...args) {
    console.log(`Calling ${name} with`, args);
    return oldValue.apply(this, args);
  };

  return descriptor;
}

const math = new Math();

// Calling add with [2, 4]
math.add(2, 4);

再完善点:

let log = (type) => {
  return (target, name, descriptor) => {
    const method = descriptor.value;
    descriptor.value =  (...args) => {
      console.info(`(${type}) 正在执行: ${name}(${args}) = ?`);
      let ret;
      try {
        ret = method.apply(target, args);
        console.info(`(${type}) 成功 : ${name}(${args}) => ${ret}`);
      } catch (error) {
        console.error(`(${type}) 失败: ${name}(${args}) => ${error}`);
      }
      return ret;
    }
  }
};

2.autobind

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

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

getPerson() === person;
// true

我们很容易想到的一个场景是 React 绑定事件的时候:

class Toggle extends React.Component {

  @autobind
  handleClick() {
	  console.log(this)
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        button
      </button>
    );
  }
}

我们来写这样一个 autobind 函数:

const { defineProperty, getPrototypeOf} = Object;

function bind(fn, context) {
  if (fn.bind) {
    return fn.bind(context);
  } else {
    return function __autobind__() {
      return fn.apply(context, arguments);
    };
  }
}

function createDefaultSetter(key) {
  return function set(newValue) {
    Object.defineProperty(this, key, {
      configurable: true,
      writable: true,
      enumerable: true,
      value: newValue
    });

    return newValue;
  };
}

function autobind(target, key, { value: fn, configurable, enumerable }) {
  if (typeof fn !== 'function') {
    throw new SyntaxError(`@autobind can only be used on functions, not: ${fn}`);
  }

  const { constructor } = target;

  return {
    configurable,
    enumerable,

    get() {

      /**
       * 使用这种方式相当于替换了这个函数,所以当比如
       * Class.prototype.hasOwnProperty(key) 的时候,为了正确返回
       * 所以这里做了 this 的判断
       */
      if (this === target) {
        return fn;
      }

      const boundFn = bind(fn, this);

      defineProperty(this, key, {
        configurable: true,
        writable: true,
        enumerable: false,
        value: boundFn
      });

      return boundFn;
    },
    set: createDefaultSetter(key)
  };
}

3.debounce

有的时候,我们需要对执行的方法进行防抖处理:

class Toggle extends React.Component {

  @debounce(500, true)
  handleClick() {
    console.log('toggle')
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        button
      </button>
    );
  }
}

我们来实现一下:

function _debounce(func, wait, immediate) {

    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

function debounce(wait, immediate) {
  return function handleDescriptor(target, key, descriptor) {
    const callback = descriptor.value;

    if (typeof callback !== 'function') {
      throw new SyntaxError('Only functions can be debounced');
    }

    var fn = _debounce(callback, wait, immediate)

    return {
      ...descriptor,
      value() {
        fn()
      }
    };
  }
}

4.time

用于统计方法执行的时间:

function time(prefix) {
  let count = 0;
  return function handleDescriptor(target, key, descriptor) {

    const fn = descriptor.value;

    if (prefix == null) {
      prefix = `${target.constructor.name}.${key}`;
    }

    if (typeof fn !== 'function') {
      throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);
    }

    return {
      ...descriptor,
      value() {
        const label = `${prefix}-${count}`;
        count++;
        console.time(label);

        try {
          return fn.apply(this, arguments);
        } finally {
          console.timeEnd(label);
        }
      }
    }
  }
}

5.mixin

用于将对象的方法混入 Class 中:

const SingerMixin = {
  sing(sound) {
    alert(sound);
  }
};

const FlyMixin = {
  // All types of property descriptors are supported
  get speed() {},
  fly() {},
  land() {}
};

@mixin(SingerMixin, FlyMixin)
class Bird {
  singMatingCall() {
    this.sing('tweet tweet');
  }
}

var bird = new Bird();
bird.singMatingCall();
// alerts "tweet tweet"

mixin 的一个简单实现如下:

function mixin(...mixins) {
  return target => {
    if (!mixins.length) {
      throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);
    }

    for (let i = 0, l = mixins.length; i < l; i++) {
      const descs = Object.getOwnPropertyDescriptors(mixins[i]);
      const keys = Object.getOwnPropertyNames(descs);

      for (let j = 0, k = keys.length; j < k; j++) {
        const key = keys[j];

        if (!target.prototype.hasOwnProperty(key)) {
          Object.defineProperty(target.prototype, key, descs[key]);
        }
      }
    }
  };
}

6.redux

实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

有了装饰器,就可以改写上面的代码。

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {};

相对来说,后一种写法看上去更容易理解。

7.注意

以上我们都是用于修饰类方法,我们获取值的方式为:

const method = descriptor.value;

但是如果我们修饰的是类的实例属性,因为 Babel 的缘故,通过 value 属性并不能获取值,我们可以写成:

const value = descriptor.initializer && descriptor.initializer();

参考

  1. ECMAScript 6 入门
  2. core-decorators
  3. ES7 Decorator 装饰者模式
  4. JS 装饰器(Decorator)场景实战

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

@fanerge
Copy link

fanerge commented Nov 29, 2018

nice

@lishihong
Copy link

mark

@zh-rocco
Copy link

赞 👍

@2507483326
Copy link

如果是方法里面是异步的,如何通过装饰器模式实现日志、方法完成时间的记录呢?

@Boboboer
Copy link

Boboboer commented Jan 1, 2019

log应用中 完善的写法 ,我这里一直报TypeError...... 你们呢

@ChengZhao100
Copy link

如果是方法里面是异步的,如何通过装饰器模式实现日志、方法完成时间的记录呢?

感觉这个可以用async await或者直接在回调里确定时间

@ChengZhao100
Copy link

ChengZhao100 commented Feb 25, 2019

log应用中 完善的写法 ,我这里一直报TypeError...... 你们呢

完善的写法调用的时候跟上面不太一样。上面是这样的:

  @log
  add(a, b) {
    return a + b;
  }

而完善的写法应该用这个:

  @log('test_add_function')
  add(a, b) {
    return a + b;
  }

@ddztomcat
Copy link

只是感觉debounce方法写的有问题,我没有验证
分析过程如下:
当我设置immediate = true时,连续点击

        if (timeout) clearTimeout(timeout); // timeout 为true 执行完这句 timeout 为 false
        if (immediate) { // 由于immediate = true 会进入
           var callNow = !timeout; //  由于timeout 为false 所以callNow 为true
            timeout = setTimeout(function(){
                timeout = null;
            }, wait) // 此时 timeout 为 true 又会重复上面的过程
            if (callNow) func.apply(context, args) // 该方法执行
}

@zgw010
Copy link

zgw010 commented Jul 7, 2019

你好, 我有个疑惑, 为什么 Object.keys() 要写成 Object["ke" + "ys"] 这种格式

@zoffyzhang
Copy link

你好,我刚刚看到这篇文章,然后翻了翻react-redux的issue,维护者说装饰器的规范不稳定,并不建议使用@connect的写法,所以我觉得你应该在文章里说明一下要谨慎使用❤️

@puck1006
Copy link

puck1006 commented Aug 3, 2020

只是感觉debounce方法写的有问题,我没有验证
分析过程如下:
当我设置immediate = true时,连续点击

        if (timeout) clearTimeout(timeout); // timeout 为true 执行完这句 timeout 为 false
        if (immediate) { // 由于immediate = true 会进入
           var callNow = !timeout; //  由于timeout 为false 所以callNow 为true
            timeout = setTimeout(function(){
                timeout = null;
            }, wait) // 此时 timeout 为 true 又会重复上面的过程
            if (callNow) func.apply(context, args) // 该方法执行
}

clearTimeout只会停止定时器 不会把值清空

@AndySkr
Copy link

AndySkr commented Feb 3, 2021

如果是方法里面是异步的,如何通过装饰器模式实现日志、方法完成时间的记录呢

判断类型 然后用promise包裹执行

@qiudaoermu
Copy link

 function debounce(wait, immediate) {
  return function handleDescriptor(target, key, descriptor) {
    const callback = descriptor.value;

    if (typeof callback !== 'function') {
      throw new SyntaxError('Only functions can be debounced');
    }

    var fn = _debounce(callback, wait, immediate)

    return {
      ...descriptor,
      value() {
        fn.apply(this) // 这里需要驾驶apply 不然当注解的方法里使用this,会找不到
      }
    };
  }
}

@KshZh
Copy link

KshZh commented Sep 10, 2021

autobind 这个装饰器,如果调用 setter 设置新函数,getter 是不是还是会返回最原始的函数?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests