ES7 装饰器(Decorator)

装饰器(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:

  1. target:当前类的原型
  2. name:当前方法/属性的名字
  3. 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对应的函数执行上下文是是装饰器调用执行上下文。

参考链接

* http://poberwong.com/2018/07/16/%E8%A3%85%E9%A5%B0%E5%99%A8-%E6%8E%A2%E7%B4%A2%E7%AE%AD%E5%A4%B4%E5%87%BD%E6%95%B0/

* http://es6.ruanyifeng.com/#docs/decorator

Leave a Comment

邮箱地址不会被公开。 必填项已用*标注