Decorator
装饰器主要用于:
- 装饰类
- 装饰方法或属性
装饰类
@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>
);
}
}
复制代码
我们来实现一下: