es7-decorator修饰器实践

阅读 1565
收藏 57
2017-08-11
原文链接:www.guofengxian.com

Es7 decorator

js飞速发展,博采众长,es7的出现,除了async await以外,另一个最令人瞩目的就是decorator修饰器了。其实decorator修饰器,是很令人迷惑的,因为你会看到类似如下的代码:

function test(target){
  target.isTestable = true; 
}
@test
class A{}

上面的代码中有个@test,没有接触过修饰器的同学,很难明白,究竟是在做什么。

下面我将带领大家,一起探索一下ES7的decortor修饰器。

一,准备工作

由于decorator是es7提供的方法,在浏览器中是无法直接运行的,所以我们需要提前作准备,对它进行编译。

1 安装基础插件

npm i babel-plugin-transform-decorators-legacy babel-register --save-dev

安装:
babel-plugin-transform-decorators-legacy 
babel-register

transform-decorators-legacy:
是第三方插件,用于支持decorators

babel-register:
用于接入node api

2 尝试使用

方法一:require hook

创建 app.js:

@hairColor
class Person { }

function hairColor(target) {
   target.hair = "white";
}

创建 complie.js

require('babel-register')({
    plugins: ['transform-decorators-legacy']
});
require("./app.js")

执行 node complie.js

其中:

app:使用了修饰器的js
complie:用来编译app
node:执行编译器complie

原理:
1,node执行complie.js文件;
2,complie文件改写了node的require方法;
3,complie在引用app.js,使用了新的require方法;
4,app.js在加载过程中被编译,并执行。

好处:
1,命令行无多余操作,与node集成了。
2,不用显式的创建.babelrc。

缺点:
1.同步编译,生产环境会有性能问题
因为不是真正输出,所以如果要使用具有修饰器的.js文件,每次都需要编译。

方法二:命令行操作

babel --plugins transform-decorators-legacy 
app.js -o app_decorator.js

其中:
babel:
    编译命令

--plugins: 
    babel可选参数

transform-decorators-legacy:
    --plugins指定的插件

app.js:
    需要编译的文件

-o:
    output缩写,编译后输出的命令

app_decorator.js:
    编译后输出的文件名

好处:
不用显式的创建.babelrc文件

缺点:
命令行中内容太多,不易维护

方法三:创建.babelrc文件

创建.babelrc

{
  "plugins": ["transform-decorators-legacy"]
}

命令行执行:

babel app.js -o app_decorator.js

上面当执行babel时,会读取配置文件.babelrc,这样命令行中就不用再写插件名了。

方法四:ts(最简单)

全局安装
sudo npm i -g typescript

tsc app.js即可

上述命令,会生成新的app.js

就是这么简单

但是会报错:
/*Experimental support for decorators is a feature
that is subject to change in a future release. 
Set the 'experimental
Decorators' option to remove this warning.*/

报错的意思是没有配置experimental 用来兼容decorator

注意,此方式:命令行中输入了需要编译的文件。

解决报错一:命令行配置

命令行:
tsc --target ES5 --experimentalDecorators

🐽:注意,此方式命令行中不可输入需要编译的文件,会直接编译当前目录。

解决报错二:创建tsconfig.json文件

{
    "noEmitOnError": true,
    "compilerOptions": {
        "experimentalDecorators": true
    }
}
noEmitOnError:如果编译不过,不输出
compilerOptions:编译选项
experimentalDecorators:是否编译修饰器

🐽:命令行中执行tsc即可
🐽:不可在命令行中输入需要编译的文件,否则将忽略tsconfig文件,千万注意。

解决报错三:-p指定tsconfig.ts

tsc -p tsconfig.json

当项目中有多种配置需求时,可以通过上面这种形式,来指定引用的tsconfig文件

ts拓展:

当命令行中不指定文件时,执行tsc:

    一,在当前目录下,搜索tsconfig.json文件,如果没找到,则去父文件夹中寻找,直到找不到。
    二,通过--project 或- p来指定需要引用的tsconfig.json

当命令行中指定文件时,执行tsc file.ts:

    会忽略tsconfig.json文件

二,修饰器

1,坎坷起步

费了这么大力气,我们终于可以使用了。

但是究竟选择哪个方案来编译?

目的:
    快速的学习decorator

如果使用ts||babel:
    第一步,编译ts为js;
    第二步,执行node helloworld.js

不难看出,这很麻烦,想要测试一个功能,需要重复两个操作,整合为一个才是我们的目标。

所以我们选择babel的node api,这样就能够同步测试了

先安装:
npm i babel-plugin-transform-decorators-legacy babel-register --save-dev

创建complie.js
require('babel-register')({
    plugins: ['transform-decorators-legacy']
});
require("./app.ts")

创建app.ts:
@addSkill
class Person {}
function addSkill(target) {
  target.say = "hello world";
}
console.log(Person.say)

最后,执行node complie.js
🐽:hello world终于出来了。

题外话:太不容易了......

2 初探(对类新增属性)

@addSkill
class Person {}
function addSkill(target) {
  target.say = "hello world";
}

回顾一下上面的例子:

@addSkill 是修饰器
Person是类

上面代码的意思:
    执行addSkill方法;
    给Person类,添加了一个静态属性say;
    say的值是”hello world“

上面的行为,相当于

class Person {}
Person = addSkill(Person) || Person;

可以看出
addSkill的参数target
就是要被修改的类

3 传递多个参数

上面我们写的修饰器,无法配置,是固定的
如果想要通过参数配置该如何处理?

解决方案:修饰器外面再封装一层函数:

例:
@wrap(1,2)
class Person {}
function wrap(num1,num2){
    return function addSkill(target) {
      target.say = "hello world";
      target.num = num1+num2;
    }
}

wrap:是修饰器包裹函数
1,2:是参数
Person:是类
addSkill才是真的修饰器

4 函数执行顺序

@wrap(1,2)
class Person {}
function wrap(num1,num2){
    console.log("wrap");
    return function addSkill(target) {
        console.log("inner");
      target.say = "hello world";
      target.num = num1+num2;
    }
}
console.log(Person.num)

上面的代码输出:
wrap
inner
3

可以得出

首先:
执行wrap方法;

然后:
执行addSkill方法;

🐽:上面只有addSkill是对类进行的操作。

5 添加实例属性

function wrap(target){
    target.prototype.say = "hello world";
}
@wrap
class Person {}
var man = new Person();

上面的代码执行console.log(man.say)
输出:hello world

6 模块化

前面有了修饰器,肯定是用来处理特定问题的,
所以封装出去

创建 decorator.ts文件

export default function(target){
    target.prototype.say = "hello world";
}

修改 app.ts文件

import decorator from "./decorator.ts";
@decorator
class Person {}
var man = new Person();
console.log(man.say);

执行 node complie.js

报错:SyntaxError: Unexpected token import

是因为import无法使用,这是es6的语法,还需要兼容

解决问题:

安装:es6的babel

npm install --save-dev babel-preset-es2015

修改:complie.js

require('babel-register')({
    "presets":["es2015"],
    "plugins": ['transform-decorators-legacy']
});
require("./app.ts")

可以看到,编译文件中,添加了一个预设,就是es2015
es2015,就是用来编译es6的

🐽:以下是版本对照

es1999 = es3
es2009 = es5
es2015 = es6
es2016 = es7
es2017 = es8

7 模块化加深

如果我们想要创建一个alien类
通过修饰器给alien类添加一个说人话的方法,该怎么实现?

首先:修改 app.ts

import decorator from "./decorator.ts";
let Person = {
    say(){
        console.log("this is Person")
    }
}
@decorator(Person)
class Alien {}
var monster = new Alien();
monster.say();

我们想要做到的事情:
Person是个对象,具备say方法;
decorator修饰器传入(Person)对象
对Alien对象进行了操作
Alien的实例monster就也具备了say方法

然后:修改decorator.ts

export default function(...list){
    return function(target){
        Object.assign(target.prototype,...list)
    }
}

ok,这样就实现了

过程是:
首先:
    decorator修饰器接受参数(Person)
然后:
    修饰器对target,也就是Alien类进行操作,
    把Person类merge进了Alien.prototype.

拓展:rest参数

上面decorator.ts中的...list
是es6的语法

...rest的意思是:
剩余所有的参数的集合

🐷注意:

...rest和rest并不相同

例:
function add(a,...rest){
    console.log(...rest)
    console.log(rest)
}
add(1,2,3,4)

上面的代码中:
rest是数组,
...rest是剩余的参数2 3 4;

注意...rest有风险:
如果只有一个元素,那可以直接使用,类似obj;
如果有多个元素,不能直接使用,类似1 2;

8 在react中使用

传统情况下:

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

以往,使用react和redux时

需要创建一个组件类,继承自react
并通过redux提供的connect,返回一个高阶组件;

使用修饰器的情况

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

好吧,好似确实容易理解一点了

这样去看的话:
可以发现,connect方法使用来处理react组件类的
同是,mapStateToProps和mapDispatchToProps都是传给修饰器外层函数的方法

最后,拿到的是对react组件修饰过后的新组件。

三,对属性的修饰

前面我们已经初步掌握了对类的修饰,继续深入。

假设:
我们有一个alien类,
它已经有了一个say方法,是说一句人话

现在,我们希望alien这个类,永远只能说一句话,彻底限制住它的学习能力。
避免对人类造成危险。

修改app.ts

import decorator from "./decorator.ts";
class Alien {
    static say(){
        console.log(1)
    }
}

Alien.say();

1,定义了一个Alien类
2,定义了一个static静态方法say
3,执行Alien.say();
4,控制台输出1;

然后修改decorator.ts

export default function(target,name,descriptor){
    descriptor.writable = false;
    return descriptor
}

当对属性修饰时

接受3个参数:
1,target是要被修饰的对象;
2,name是属性名;
3,descriptor是对属性的描述;

这里很类似,es5的
Object.defineProperty(Person.prototype, 'name', descriptor);

再回来修改app.ts

import decorator from "./decorator.ts";
class Alien {
    @decorator
    static say(){
        console.log(1)
    }
}

Alien.say();

1,添加了一句@decorator
2,目的是对属性进行修饰
3,执行Alien.say()
4,控制台还是输出1;

我们的目的是让它只能说1。

所以再次修改app.ts

import decorator from "./decorator.ts";
class Alien {
    @decorator
    static say(){
        console.log(1)
    }
}
Alien.say();
Alien.say = function(){
    console.log(2);
}
Alien.say();

1,修改了say方法
2,并执行新的say方法
3,控制台报错!!!!!!!!!

//TypeError: Cannot assign to read only property 'say' of function 'function Alien()

提示我们:say属性read only不可修改

🐷注意:
read only不是不允许赋值,
而是赋值一次之后,不允许修改!切记!

验证一下:再次修改decorator

export default function(target,name,descriptor){
    descriptor.writable = true;
    return descriptor
}

1,修改属性的可写属性为true
2,再次执行app.ts
3,命令行输出1,2

说明我们的修饰正确!

拓展:static和state

1,static是类的静态方法,不是实例的方法

例:
class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

2,static能够被子类继承

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

3,state是用来定义类的静态属性的

例:
class ReactCounter extends React.Component {
  state = {
    count: 0
  };
}

四,实现一个log装饰器

修改app.ts

例:
import decorator from "./decorator.ts";
class Math {
  @decorator
  add(a, b) {
    return a + b;
  }
}
const math = new Math();
math.add(2, 4);

1,定义了一个Math类
2,通过decorator修饰了add属性
3,目的在调用add方法的时候,记录log

修改decorator.ts

export default function(target,name,descriptor){
    var oldValue = descriptor.value;
    descriptor.value = function() {
      console.log(`Calling "${name}" with`, arguments);
      return oldValue.apply(null, arguments);
    };
    return descriptor;
}

1,暴露decorator方法
2,descriptor.value其实就是add(a,b){return a+b}这个函数
3,存储add函数为oldValue
4,修改add属性对应的方法,在其中添加console.

五,修饰器的执行顺序

例:
function dec(id){
    console.log('evaluated', id);
    return (target, property, descriptor) => console.log('executed', id);
}

class Example {
    @dec(1)
    @dec(2)
    method(){}
}

上面的例子,输出
1
2
2
1

原理类似:
function test(num){
    console.log(num);
    return function(){
        console.log(num);
    }
}
undefined
var fn1 = test(1);
var fn2 = test(2);
fn1(fn2());
欣赏此文?求鼓励,求支持! 丰羡 WeChat Pay

微信打赏

丰羡 Alipay

支付宝打赏

评论