装饰器(Decorator)
Decorator 提案经过了大幅修改,目前还没有定案,不知道语法会不会再变。下面的内容完全依据以前的提案,已经有点过时了。等待定案以后,需要完全重写。
装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。许多面向对象的语言都有这项功能,目前有一个提案将其引入了 ECMAScript。
装饰器是一种函数,写成@ + 函数名
。它可以放在类和类方法、属性的定义前面。
@frozen class Foo { @configurable(false) @enumerable(true) method() {} @throttle(500) expensiveMethod() {} }
更多知识可参考:http://es6.ruanyifeng.com/#docs/decorator
应用场景
其实很多时候装饰器可以充当高阶函数的作用,用来给原来的函数或者类增加一些新的能力。最近开发中遇到了一个应用场景,就采用装饰器来实践下。
ant design里,input、select 等输入组件,都有onchange方法,但是比较麻烦是是,input 的onchange第一个参数是value,即输入的值;而select 的onchange第一个参数是event,需要多写一行代码,value=event.target.value
,最重要的是,很多组件,我根本记不住哪个组件是value,哪个是event,多数时候需要打印出来看一下。实在不能忍,突然想到了装饰器,是不是我们可以写一个装饰器,统一把第一个参数归一化为value呢?当然可以。
如何写一个装饰器呢
装饰器写法很简单,它接受三个参数,并且返回descriptor:
- target:当前类的原型
- name:当前方法/属性的名字
- descriptor:将被定义或修改的属性描述符,可参考Object.defineProperty
descriptor.value 就是被修饰的方法,官方大致的思路很明确,就是利用常规的 hook 方法,对 value 进行 hook 改造,达到装饰的目的。
我们要写的装饰器大概形状应该是这样的。
function formatValue (target, name, descriptor) { // ...something return { ...descriptor, value: function() { // ... } } }
开始写代码
参考了网上一些文章,开始写其实很简单,马上就能实现我们想要的代码。
function formatValue (target, name, descriptor) { const fn = descriptor.value; return { ...descriptor, value:(...args) =>{ const len = args.length; if (len > 0 && args[len - 1].target) { args[len - 1] = args[len - 1].target.value; } fn.apply(this, args); } } }
组件代码
class extends React.PureComponent { @formatValue handleActionChange=(value)=> { this.setState({action: value}); } render () {} }
然而这段代码并不work, 把descriptor 打印出来看,发现并没有value 属性,倒是多了个initializer,什么鬼?
{ configurable: true, enumerable: true, writable: true, initializer:f() }
查资料并且结合着猜想,会不会是因为不支持箭头函数?果然,当我把handleActionChange改为普通函数声明的时候,value 值就能拿到了,但initializer不见了?事情有些蹊跷,接着猜想,会不会是因为箭头函数的定义方式,其实是一个赋值表达式,并不是函数声明表达式,本质是把一个匿名函数赋值给了handleActionChange这个属性。那会不会是装饰器对属性的装饰和对方法的装饰有区别呢?顺着这个思路查资料,果然找到了答案。
装饰器在装饰属性的时候,先通过执行调用 initializer
来获取属性默认值(此时是不存在 value 的),然后配合 getter 和 setter 来装饰这个属性本身。所以我们修改代码如下:
export function formatInputValue (target, name, descriptor) { const fn = descriptor.initializer; return { ...descriptor, initializer: (...args) =>{ const len = args.length; if (len > 0 && args[len - 1].target) { args[len - 1] = args[len - 1].target.value; } fn.apply(this, args); } }; }
但其实这个函数还有问题,因为箭头函数绑定的this,是定义时的this,普通函数里为undefined。而我们其实要的效果,是绑定了组件执行上下文的this,所以修改为:
export function formatInputValue (target, name, descriptor) { const fn = descriptor.initializer; return { ...descriptor, initializer: function (...args) { const len = args.length; if (len > 0 && args[len - 1].target) { args[len - 1] = args[len - 1].target.value; } fn.apply(this, args); } }; }
现在终于可以执行成功了。但这个函数还是比较局限的,因为目前只有被装饰的函数是箭头函数才能正确返回,如果装饰的的普通的函数方法,那还是不符合预期,所以我们再兼容一下。
export function formatInputValue (target, name, descriptor) { let fn; function newFn (...args) { const len = args.length; if (len > 0 && args[len - 1].target) { args[len - 1] = args[len - 1].target.value; } fn.apply(this, args); } // 装饰属性的情况下,以此可以获取实例化的时候此属性的默认值 if (descriptor.initializer) { return { ...descriptor, initializer: function () { // 不用箭头函数,绑定运行时的this fn = descriptor.initializer.call(this); return newFn; }, }; } else { // 装饰方法的情况下 fn = descriptor.value; return { ...descriptor, value: newFn, }; } }
这样就可以愉快的使用装饰器了。最后要注意的一点就是this 的指向问题,只有initializer和value对应的函数执行上下文是是装饰器调用执行上下文。
参考链接