Decorator

Decorators are mainly used for:

  1. Decoration class
  2. Decorates a method or property

Decoration class

@annotation
class MyClass {}function annotation(target) {
   target.annotated = true;
}
Copy the code

Decorates a method or property

class MyClass {
  @readonly
  method() { }
}

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

Babel

Install the compiled

You can check out Babel’s compiled code at Try It Out on the Babel website.

However, we can also choose to compile locally:

npm init

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

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

Create a new.babelrc file

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

Recompile the specified file

babel decorator.js --out-file decorator-compiled.js
Copy the code

Decorator class compilation

Compile the front:

@annotation
class MyClass {}function annotation(target) {
   target.annotated = true;
}
Copy the code

The compiled:

var _class;

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

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

We can see that for class decoration, the principle is:

@decorator
class A {}

/ / is equivalent to

class A {}
A = decorator(A) || A;
Copy the code

Compilation of decorator methods

Compile the front:

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 the code

The compiled:

var _class;

function _applyDecoratedDescriptor(target, property, decorators, descriptor, context ) {
	/** * Part 1 * Copy attributes */
	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;
	}

	/** * Part 2 * Apply multiple decorators */
	desc = decorators
		.slice()
		.reverse()
		.reduce(function(desc, decorator) {
			return decorator(target, property, desc) || desc;
		}, desc);

	/** * Part 3 * sets the attributes to be 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 the code

Compiled source code parsing of decorator methods

We can see that Babel builds a _applyDecoratedDescriptor function to decorate the method.

Object.getOwnPropertyDescriptor()

At the time of the incoming parameters, we used a Object. The getOwnPropertyDescriptor () method, we’ll look at this method:

Object. GetOwnPropertyDescriptor () method returns the specified Object on a has its own corresponding attribute descriptor. (Own properties are those that are directly assigned to the object and do not need to be looked up on the stereotype chain.)

Note by the way that this is an ES5 method.

Here’s an 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 the code

The first part source code analysis

In internal _applyDecoratedDescriptor function, we will first Object. GetOwnPropertyDescriptor () returns the property descriptor Object did a copy:

// Copy a descriptor
var desc = {};
Object["ke" + "ys"](descriptor).forEach(function(key) { desc[key] = descriptor[key]; }); desc.enumerable = !! desc.enumerable; desc.configurable = !! desc.configurable;// If there is no value attribute or initializer attribute, it indicates getter and setter
if ("value" in desc || desc.initializer) {
	desc.writable = true;
}
Copy the code

So what is the Initializer property? Object. GetOwnPropertyDescriptor () returns the Object does not have this property, and, indeed, this is the Babel of Class in order to cooperate with the decorator and produce an attribute, for example with the code below:

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 the code

Babel will compile to:

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

The descriptor passed in _applyDecoratedDescriptor has the initializer property.

The second part source code analysis

Next, apply multiple decorators:

/** * part 2 * @type {[type]} */
desc = decorators
	.slice()
	.reverse()
	.reduce(function(desc, decorator) {
		return decorator(target, property, desc) || desc;
	}, desc);
Copy the code

Apply multiple decorators to a method, such as:

class MyClass {
  @unenumerable
  @readonly
  method() { }
}
Copy the code

Babel will compile to:

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

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

The third part source code analysis

/** * Part 3 * sets the attributes to be 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 the code

If desc has the initializer attribute, it means that when you decorate a class attribute, value will be set to:

desc.initializer.call(context)
Copy the code

Context is _class.prototype. Call (context), which makes sense because it’s possible

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

  getNum() {
    return 1; }}Copy the code

Finally, either the decorator method or the property executes:

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

As you can see, the decorator method is essentially implemented using Object.defineProperty().

application

1.log

To add the log function to a method, check the input arguments:

class Math {
  @log
  add(a, b) {
    returna + 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(a);// Calling add with [2, 4]
math.add(2.4);
Copy the code

To perfect:

let log = (type) = > {
  return (target, name, descriptor) = > {
    const method = descriptor.value;
    descriptor.value =  (. args) = > {
      console.info(` (${type}) is executing:${name}(${args}) = ?`);
      let ret;
      try {
        ret = method.apply(target, args);
        console.info(` (${type}Success:${name}(${args}) = >${ret}`);
      } catch (error) {
        console.error(` (${type}Failure) :${name}(${args}) = >${error}`);
      }
      returnret; }}};Copy the code

2.autobind

class Person {
  @autobind
  getPerson() {
  	return this; }}let person = new Person();
let { getPerson } = person;

getPerson() === person;
// true
Copy the code

One 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 the code

Let’s write an autobind function like this:

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 (typeoffn ! = ='function') {
    throw new SyntaxError(`@autobind can only be used on functions, not: ${fn}`);
  }

  const { constructor } = target;

  return {
    configurable,
    enumerable,

    get() {

      / * * * use this approach is to replace the function, so that when such as * Class in the prototype. The hasOwnProperty (key), in order to correctly return * so made this judgement here * /
      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 the code

3.debounce

Sometimes, we need to take precautions against the execution method:

class Toggle extends React.Component {

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

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

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) {
            varcallNow = ! 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 (typeofcallback ! = ='function') {
      throw new SyntaxError('Only functions can be debounced');
    }

    var fn = _debounce(callback, wait, immediate)

    return{... descriptor, value() { fn() } }; }}Copy the code

4.time

Time used for statistical method execution:

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 (typeoffn ! = ='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 the code

5.mixin

Methods used to blend objects into a Class:

constSingerMixin = { 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 the code

A simple implementation of mixins 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 the code

6.redux

In real development, React and the Redux library are often written like this.

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Copy the code

With decorators, you can rewrite the code above.

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

The latter seems relatively easy to understand.

7. Pay attention to

All of the above are used to modify class methods. We get values as follows:

const method = descriptor.value;
Copy the code

But if we were modifying an instance property of the class, and we can’t get a value from the value property because of Babel, we could write:

const value = descriptor.initializer && descriptor.initializer();
Copy the code

reference

  1. ECMAScript introduction to 6
  2. core-decorators
  3. ES7 Decorator Decorator pattern
  4. JS Decorator scene in action

ES6 series

ES6 directory address: github.com/mqyqingfeng…

ES6 series is expected to write about 20 chapters, aiming to deepen the understanding of ES6 knowledge points, focusing on the block-level scope, tag template, arrow function, Symbol, Set, Map and Promise simulation implementation, module loading scheme, asynchronous processing and other contents.

If there is any mistake or not precise place, please be sure to give correction, thank you very much. If you like or are inspired by it, welcome star and encourage the author.