5

装饰器 - 表面上理解是 用来装饰个啥东西的东西
是指这个东西本来就有,只是通过什么方式或渠道 加工装饰了一下
常被用于插件的封装

常见应用

间谍装饰

一般用于单元测试

  • 定义一个普通的函数
function f(a, b) {
    alert(a + b)
}
  • 想要封装该函数,使其能够多出一些没有的功能
function spy(func) { // 传入函数,返回一个新的函数
    return function(...args) {// 外部执行函数func时的参数
        let _args = args.push('i')
        func.apply(this, _args)
    }
}

f = spy(f) // 通过装饰
f(1,2) // alert出来是1,2 ,i

延时装饰

  • 还是上面那个普通的参数f
  • 想封装一个函数delay,传入毫秒时间后,毫秒后执行该普通函数f
function delay(func, ms) {
    return function(...args) {
        setTimeout(() => {
            func.apply(this, args)
        }, ms)
    }
}

f1000 = delay(f, 1000) // 1s后执行
f5000 = delay(f, 5000) // 5s后执行

去抖装饰

  • 还是上面那个普通的参数f
  • 想封装一个函数,f函数执行之后的一小段时间内的继续执行操作,不触发f函数执行
  • 就是类似于一个按钮在2秒钟内点20下,也只执行一下
function debounce(func, ms) {
    let isCool = false // 是否为冻结状态
    return function(...args) {
        if (isCool) return // 冻结状态直接跳出不继续执行
        // 非冻结状态
        func.apply(this, args) // 执行
        isCool = false // 执行后把状态置为冻结状态
        setTimeOut(() => {isCool = true}, ms) // 过ms时间后再把状态置为解冻
    }
}

let p5 = debounce(f, 5000)

节流装饰

  • 还是上面那个普通的参数f
  • 想封装一个函数,f函数执行之后的一小段时间内的继续执行操作,不触发f函数执行,但ms时间到了以后会自动执行这段时间中最后一次触发的操作
  • 就是类似于鼠标拖动,在1s内可能触发200次,取最后一次的结果执行
function throttle(func, ms) {
    let isCool = false,
    _self,
    _args;
    function w(...args) {
        if (isCool) {
            _self = this
            _args = args
            return
        } // 如果冻结状态,直接返回,并记录下来传递的参数
        // 非冻结状态
        func.apply(this, args) // 执行
        isCool =true // 执行后冻结
        setTimeout(() => {
            isCool = false // 时间到了之后解冻
           if (_args && _args.length) { // ms时间过后,如果在冻结期有触发操作,则执行一次
                w.apply(_self, _args)
                _self = null
                _args = null // 执行后把冻结期参数置空,避免ms之后对之前的操作进行下一轮执行
           } 
        }, ms)
        
    }
    return w
}

let w5 = throttle(f, 5000)

ES6中的装饰器Decorator

装饰器的盖面在ES6和Typescript中被引入了出来,但目前尚未定案,语法可能还会变,不过装饰器已经被广泛应用

定义:Decorator是一种与类相关的语法,在很多语言都有应用,用来注释或修改类和类方法

写法:@+函数名

类的装饰

//定义一个函数,也就是定义一个Decorator,target参数就是传进来的Class。
//这里是为类添加了一个静态属性
function testable(target) {
  target.isTestable = true;
}

@testable
class MyTestableClass {}

console.log(MyTestableClass.isTestable) // true

加参数

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

下面是修改类的prototype对象

function testable(target) {
  target.prototype.isTestable = true;
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable // true

方法的装饰

方法装饰器的第一个参数是类的原型对象,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);第二个参数是所要装饰的属性名,第三个参数是该属性的描述对象。

function readonly(target, name, descriptor){
  descriptor.writable = false; // 此处参看Object.defineProperty将修改属性关闭,意味着该对象属性只读
  return descriptor;
}

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

再来个例子:

//定义一个Class并在其add上使用了修饰器
class Math {
  @log
  add(a, b) {
    return a + b;
  }
}

//定义一个修饰器
function log(target, name, descriptor) {
  //这里是缓存旧的方法,也就是上面那个add()原始方法
  var oldValue = descriptor.value;

  //这里修改了方法,使其作用变成一个打印函数
  //最后依旧返回旧的方法,真是巧妙
  descriptor.value = function() {
    console.log(`Calling "${name}" with`, arguments);
    return oldValue.apply(null, arguments);
  };

  return descriptor;
}

const math = new Math();
math.add(2, 4); // 先是打印了Calling add with [2,4],后返回了[2,4]

执行顺序

多个装饰器可以同时应用到一个声明上

  • 书写在同一行上:

    @f @g x
  • 书写在多行上:

    @f
    @g
    x
function readonly(target, name, descriptor){
  descriptor.writable = false; // 此处参看Object.defineProperty将修改属性关闭,意味着该对象属性只读
  console.log('readonly - 1')
  return () => {
    console.log('readonly - 2')
  };
}
function test(target, name, descriptor) {
  console.log('test - 1')
  return () => {
    console.log('test - 2')
  }
}
@testable
class Person {
  @readonly
  @test
  name() { return `${this.first} ${this.last}` }
}
// readonly - 1
// test - 1
// test - 2
// readonly - 2

参数装饰器(Typescript)

两个参数:

  1. 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  2. 成员的名字。
  3. 参数在函数参数列表中的索引。

参数装饰器只能用来监视一个方法的参数是否被传入。参数装饰器的返回值会被忽略。

function required(target: any, propertyName: string, index: number) {
  console.log(`现在修饰的是${propertyName}的第${index}个参数`)
}

class ClassI {
  public name: string = 'lisa'
  public age: number = 18
  public getInfo(prefix: string,  @required infoType: string): any {
    console.log(prefix + ' ' + this[infoType])
    return prefix + ' ' + this[infoType]
  }
}
interface ClassI {
  [key: string]: string | number | Function
}
const i = new ClassI()
console.log(i)
i.getInfo('*', 'age')

// [LOG]: "现在修饰的是getInfo的第1个参数" 
// [LOG]: ClassI: { "name": "lisa", "age": 18 } 
// [LOG]: "* 18" 

React中的例子

//假如有这么一个页面组件,用于显示用户资料的,当从Home组件进去到这个组件时
//希望title从“Home Page”变成“Profile Page”
//注意这里隐形传入了组件,语法类似setTitle('Profile Page')(Profile)
@setTitle('Profile Page')
class Profile extends React.Component {
    //....
}
//开始定义装饰器
//看到两个箭头函数感觉懵逼了,转化一个也就是一个函数里返回一个函数再返回一个组件包裹器而已
//title参数对应上面的“Profile Page”字符串
//WrappedComponent参数对应上面的Profile组件
//然后在组件加载完修改了title,在返回一个新组件,是不是很像高阶组件呢
const setTitle = (title) => (WrappedComponent) => {
   return class extends React.Component {
      componentDidMount() {
          document.title = title
      }
      render() {
         return <WrappedComponent {...this.props} />
      }
   }
}

vue-property-decorator

在使用TS和Vue开发的过程中我们经常使用vue-property-decorator这个库,它封装了@Component、@Prop、@Watch、@Emit等常用装饰器

@Component

@Component装饰器其实是vue-class-component库提供的,首先来看它的定义,@Component是一个类装饰器,类装饰器接收类的构造函数作为入参。

// index.ts
function Component (options: ComponentOptions<Vue> | VueClass<Vue>): any {
  if (typeof options === 'function') {
    return componentFactory(options)
  }
  return function (Component: VueClass<Vue>) {
    return componentFactory(Component, options)
  }
}

@Component有一个入参options,是为了方便用户对组件类进行一些额外属性的声明,内部判断options是否是函数以区分不同的调用方式:

// 默认传入类构造函数
@Component
export default class HelloWorld extends Vue {}

// 传入options对象
@Component({name: 'HelloWorld'})
export default class HelloWorld extends Vue {}
复制代码

接下来调用了工厂函数componentFactory,实际上componentFactory干了几件事:

  • 将类原型上的属性按照不同类型(data、methods、mixins、computed)添加到options中
  • 将mixins中的data属性依赖收集
  • 返回一个传入options的新构造器
function componentFactory(Component, options = {}) {
    options.name = options.name || Component._componentTag || Component.name;
    // prototype props.
    const proto = Component.prototype;
    // 按类型添加到options
    Object.getOwnPropertyNames(proto).forEach(function (key) {
        if (key === 'constructor') {
            return;
        }
        // 判断传入属性是否在白名单
        if ($internalHooks.indexOf(key) > -1) {
            options[key] = proto[key];
            return;
        }
        const descriptor = Object.getOwnPropertyDescriptor(proto, key);
        if (descriptor.value !== void 0) {
            // methods
            if (typeof descriptor.value === 'function') {
                (options.methods || (options.methods = {}))[key] = descriptor.value;
            }
            else {
                // typescript decorated data
                (options.mixins || (options.mixins = [])).push({
                    data() {
                        return { [key]: descriptor.value };
                    }
                });
            }
        }
        else if (descriptor.get || descriptor.set) {
            // computed properties
            (options.computed || (options.computed = {}))[key] = {
                get: descriptor.get,
                set: descriptor.set
            };
        }
    });
    // 依赖收集
    (options.mixins || (options.mixins = [])).push({
        data() {
            return collectDataFromConstructor(this, Component);
        }
    });
    // 将类中添加装饰器的方法取出来执行后删除__decorators__,填充__decorators__的逻辑下方介绍@Prop时有提及
    const decorators = Component.__decorators__;
    if (decorators) {
        decorators.forEach(fn => fn(options));
        delete Component.__decorators__;
    }
    // 找到父类并创建一个新的构造器
    const superProto = Object.getPrototypeOf(Component.prototype);
    const Super = superProto instanceof Vue
        ? superProto.constructor
        : Vue;
    const Extended = Super.extend(options);
    // 检测options中key值合法性
    forwardStaticMembers(Extended, Component, Super);
    // 元编程相关
    if (reflectionIsSupported()) {
        copyReflectionMetadata(Extended, Component);
    }
    return Extended;
}
复制代码

从@Component中我们可以看到类装饰器可以通过对构造器的修改来为原有类做无感知的修改,装饰器是在编译时作用的,所以无法影响类的实例,但是可以通过修改类的原型来影响实例。

@Prop

我们通过@Prop来学习属性装饰器,@Prop是一个属性装饰器,实现为一个装饰器工厂,return的函数参数有两个,一个是类的构造函数或者原型对象,一个是装饰的成员名称。

function Prop(options) {
    // 保证options是一个对象
    if (options === void 0) { options = {}; }
    // 返回一个工厂函数,target对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。key是成员的名字
    return function (target, key) {
        // 获取元数据
        applyMetadata(options, target, key);
        // 高阶函数,作用是设置组件的props
        createDecorator(function (componentOptions, k) {
            ;
            (componentOptions.props || (componentOptions.props = {}))[k] = options;
        })(target, key);
    };
}
// 判断是否支持反射API
var reflectMetadataIsSupported = typeof Reflect !== 'undefined' && typeof Reflect.getMetadata !== 'undefined';
function applyMetadata(options, target, key) {
    // 如果支持反射API,取出相应元数据
    if (reflectMetadataIsSupported) {
        if (!Array.isArray(options) &&
            typeof options !== 'function' &&
            typeof options.type === 'undefined') {
            options.type = Reflect.getMetadata('design:type', target, key);
        }
    }
}
// 高阶函数,将装饰器都push到@Component的__decorators__中,@Prop是将设置props的方法push进去
function createDecorator(factory) {
    return (target, key, index) => {
        const Ctor = typeof target === 'function'
            ? target
            : target.constructor;
        if (!Ctor.__decorators__) {
            Ctor.__decorators__ = [];
        }
        if (typeof index !== 'number') {
            index = undefined;
        }
        Ctor.__decorators__.push(options => factory(options, key, index));
    };
}

复制代码

我看到这个createDecorator方法时震惊于包作者的机智,包中几乎所有的属性装饰器都是基于这个函数实现的,包括Inject、InjectReactive、Provide、ProvideReactive、Model、Prop、PropSync、Watch、Ref,这个高阶函数能将对属性装饰器的操作都Push到一个数组中,再由@Component取出执行并统一修改装饰的类实例,并且由于闭包的关系塞进@Component数组中的方法都拥有外部属性的访问权限,可以将不同的属性装饰器写在各自的方法中。(换我来写这一段大概就是push一个{type: 'Prop'}进去了,然后执行的时候做if判断,在大佬这里学到了策略模式的更高阶玩法,有用的知识增加了!)

@Emit

我们再通过@Emit来学习一下方法装饰器,方法装饰器作用在方法的属性描述符上,可以用来监视,修改或者替换方法定义,接收三个参数,前两个参数和@Prop一样,分别是构造函数(static方法)或原型对象(实例方法)、方法的名称,多了一个参数是参数的属性描述符(descriptor),这个属性描述符很关键,其中的value属性就是被装饰的方法最后真正要执行的方法。

// 用于将驼峰命名的方法名转变为减号连接
var hyphenateRE = /\B([A-Z])/g;
var hyphenate = function (str) { return str.replace(hyphenateRE, '-$1').toLowerCase(); };

function Emit(event) {
    return function (_target, key, descriptor) {
        key = hyphenate(key);
        // 保存原有函数
        var original = descriptor.value;
        descriptor.value = function emitter() {
            var _this = this;
            var args = [];
            // arguments 是触发这个emit方法的事件列表
            for (var _i = 0; _i < arguments.length; _i++) {
                args[_i] = arguments[_i];
            }
            // 执行vue的$emit方法,并传入参数
            var emit = function (returnValue) {
                if (returnValue !== undefined)
                    args.unshift(returnValue);
                _this.$emit.apply(_this, [event || key].concat(args));
            };
            // 执行原方法
            var returnValue = original.apply(this, args);
            // 如果原方法返回了promise,执行.then方法后emit,如果是正常返回值就直接emit
            if (isPromise(returnValue)) {
                returnValue.then(function (returnValue) {
                    emit(returnValue);
                });
            }
            else {
                emit(returnValue);
            }
            // 将原方法的返回值返回
            return returnValue;
        };
    };
}
复制代码

@Emit的核心思路就是截胡函数的执行,在被装饰的函数被调用后将会进入方法装饰器,在装饰器中进行额外的emit操作,使用者就不再需要手动去调用this.$emit了。

方法装饰器在业务中使用场景比较多,例如我们可以将日志上报或者埋点处理等操作封装在方法装饰器中,再用方法装饰器去修饰相应的方法,这个方法就会得到功能上的增强,而不用改变方法本身的逻辑,装饰器和方法的逻辑解耦,大大增加了代码的可读性和灵活度,也可以少些一些重复的代码了。

参考文档:

https://es6.ruanyifeng.com/#docs/decorator#%E6%96%B9%E6%B3%95%E7%9A%84%E8%A3%85%E9%A5%B0

https://www.tslang.cn/docs/handbook/decorators.html

https://juejin.im/post/6844904093404626957#heading-0


chidaozhi
60 声望4 粉丝

前端老阿姨