Decorator
Decorators are mainly used for:
- Decoration class
- 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
- ECMAScript introduction to 6
- core-decorators
- ES7 Decorator Decorator pattern
- 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.