Home > Web Front-end > JS Tutorial > body text

A brief discussion on decorators in ES6

不言
Release: 2018-11-15 17:35:14
forward
2493 people have browsed it

This article brings you a brief discussion of decorators in ES6. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.

Decorator

Decorator is mainly used for:

  1. Decoration class

  2. Decoration method or attribute

Decoration class

@annotation
class MyClass { }

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

Decoration method or attribute

class MyClass {
  @readonly
  method() { }
}

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

Babel

Installation and compilation

We can install it in Babel Try it out on the official website to view the Babel compiled code.

But we can also choose local compilation:

npm init

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

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

Create a new .babelrc file

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

Compile the specified file

babel decorator.js --out-file decorator-compiled.js
Copy after login

Compilation of decoration classes

Before compilation:

@annotation
class MyClass { }

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

After compilation:

var _class;

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

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

We can see the decoration of the class, the principle is:

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;
Copy after login

Compilation of decoration methods

Before compilation:

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;
}
Copy after login

After compilation:

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;
}
Copy after login

Compiled source code analysis of decoration method

We can see that Babel has built an _applyDecoratedDescriptor function for Decorate the method.

Object.getOwnPropertyDescriptor()

When passing in parameters, we use an Object.getOwnPropertyDescriptor() method. Let’s take a look at this method:

Object. The getOwnPropertyDescriptor() method returns the property descriptor corresponding to an own property on the specified object. (Own properties refer to properties that are directly assigned to the object and do not need to be looked up from the prototype chain)

By the way, please note that this is an ES5 method.

For example:

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,
// }
Copy after login

The first part of the source code analysis

Inside the _applyDecoratedDescriptor function, we first make a property descriptor object returned by Object.getOwnPropertyDescriptor() Copy:

// 拷贝一份 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;
}
Copy after login

So what is the initializer attribute? The object returned by Object.getOwnPropertyDescriptor() does not have this property. Indeed, this is a property generated by Babel's Class in order to cooperate with the decorator. For example, for the following code:

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

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

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

Babel will Compiled as:

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

At this time, the descriptor passed into the _applyDecoratedDescriptor function has the initializer attribute.

The second part of source code analysis

The next step is to apply multiple decorators:

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

For one method, multiple decorators are applied, such as:

class MyClass {
  @unenumerable
  @readonly
  method() { }
}
Copy after login

Babel will compile to:

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

In the second part of the source code, reverse() and reduce() operations are performed. From this, we can also find that if the same method has multiple decorators, it will be Execute from the inside out.

Part 3 Source Code Analysis

/**
 * 第三部分
 * 设置要 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;
Copy after login

If desc has an initializer attribute, it means that when the class attribute is decorated, the value of value will be set to:

desc.initializer.call(context)
Copy after login

The value of context is _class.prototype. The reason why we need to call(context) is also easy to understand, because it is possible

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

  getNum() {
    return 1;
  }
}
Copy after login

In the end, whether it is a decoration method Or attributes, will be executed:

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

It can be seen that the decoration method is essentially implemented using Object.defineProperty().

Application

1.log

Add the log function to a method and check the input parameters:

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);
Copy after login

More perfect:

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;
    }
  }
};
Copy after login

2.autobind

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

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

getPerson() === person;
// true
Copy after login

A scenario we can easily think of is when React binds events:

class Toggle extends React.Component {

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

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

Let’s write such an autobind function:

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)
  };
}
Copy after login

3 .debounce

Sometimes, we need to perform anti-shake processing on the executed method:

class Toggle extends React.Component {

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

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

Let’s implement it:

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()
      }
    };
  }
}
Copy after login

4.time

Used to count method execution 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);
        }
      }
    }
  }
}
Copy after login

5.mixin

Used to mix object methods into 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"
Copy after login

A simple implementation of mixin is as follows:

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]);
        }
      }
    }
  };
}
Copy after login

6.redux

In actual development, when React is used in combination with the Redux library, it is often necessary to write it as follows.

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Copy after login

With the decorator, you can rewrite the above code.

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

Relatively speaking, the latter way of writing seems easier to understand.

7. Note

The above are all used to modify class methods. The way we get the value is:

const method = descriptor.value;
Copy after login

But if we modify the instance attribute of the class, Because of Babel, the value cannot be obtained through the value attribute. We can write it as:

const value = descriptor.initializer && descriptor.initializer();
Copy after login

The above is the detailed content of A brief discussion on decorators in ES6. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:segmentfault.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template