>  기사  >  웹 프론트엔드  >  ES6의 데코레이터에 대한 간략한 토론

ES6의 데코레이터에 대한 간략한 토론

不言
不言앞으로
2018-11-15 17:35:142473검색

이 기사의 내용은 ES6의 데코레이터에 대해 간략하게 설명하는 내용입니다. 참고할 가치가 있는 친구들이 참고할 수 있기를 바랍니다.

Decorator

데코레이터는 주로 다음 용도로 사용됩니다.

  1. Decoration 클래스

  2. Decoration 메서드 또는 속성

Decoration 클래스

@annotation
class MyClass { }

function annotation(target) {
   target.annotated = true;
}

Decoration 메서드 또는 속성

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;
}

그럼 초기화 속성은 무엇인가요? ? Object.getOwnPropertyDescriptor()에 의해 반환된 객체에는 이 속성이 없습니다. 실제로 이것은 데코레이터와 협력하기 위해 Babel의 클래스에서 생성된 속성입니다. 예를 들어 다음 코드의 경우:

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 함수에 전달된 디스크립터에는 초기화 속성이 있습니다.

소스 코드 분석의 두 번째 부분

다음 단계는 여러 데코레이터를 적용하는 것입니다.

/**
 * 第二部分
 * @type {[type]}
 */
desc = decorators
    .slice()
    .reverse()
    .reduce(function(desc, decorator) {
        return decorator(target, property, desc) || desc;
    }, desc);
메서드의 경우 다음과 같이 여러 데코레이터가 적용됩니다.

class MyClass {
  @unenumerable
  @readonly
  method() { }
}
Babel은 다음과 같이 컴파일됩니다.

_applyDecoratedDescriptor(
    _class.prototype,
    "method",
    [unenumerable, readonly],
    Object.getOwnPropertyDescriptor(_class.prototype, "method"),
    _class.prototype
)
소스에서 두 번째 부분의 코드에서는 reverse() 및 Reduce() 작업이 수행되었습니다. 이를 통해 동일한 메서드에 여러 개의 데코레이터가 있는 경우 내부에서 실행된다는 것도 알 수 있습니다.

3부 소스 코드 분석

/**
 * 第三部分
 * 设置要 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에 초기화 속성이 있으면 클래스의 속성이 장식되면 value의 값이 다음으로 설정된다는 의미입니다.

desc.initializer.call(context)
그리고 context의 값은 입니다. _class.prototype, call(context)를 하는 이유도 가능하기 때문에 이해하기 쉽습니다

class MyClass {
  @readonly
  value = this.getNum() + 1;

  getNum() {
    return 1;
  }
}
_class.prototype,之所以要 call(context),这也很好理解,因为有可能
Object["define" + "Property"](target, property, desc);

最后无论是装饰方法还是属性,都会执行:

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);

由此可见,装饰方法本质上还是使用 Object.defineProperty()결국 장식적인 방법인지 속성인지는

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;
    }
  }
};

메서드가 기본적으로 Object.defineProperty()를 사용하여 구현되는 것을 볼 수 있습니다.

Application

1.log

메서드에 로그 함수를 추가하고 입력 매개변수를 확인하세요.

class Person {
  @autobind
  getPerson() {
      return this;
  }
}

let person = new Person();
let { getPerson } = person;

getPerson() === person;
// true

더 완벽한:

class Toggle extends React.Component {

  @autobind
  handleClick() {
      console.log(this)
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        button
      </button>
    );
  }
}

2.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)
  };
}

우리가 쉽게 생각할 수 있는 시나리오는 React 바인딩 이벤트입니다. :

class Toggle extends React.Component {

  @debounce(500, true)
  handleClick() {
    console.log('toggle')
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        button
      </button>
    );
  }
}

다음과 같이 자동 바인딩 기능을 작성해 보겠습니다.

function _debounce(func, wait, immediate) {

    var timeout;

    return function () {
        var context = this;
        var args = arguments;

        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(function(){
                timeout = null;
            }, wait)
            if (callNow) func.apply(context, args)
        }
        else {
            timeout = setTimeout(function(){
                func.apply(context, args)
            }, wait);
        }
    }
}

function debounce(wait, immediate) {
  return function handleDescriptor(target, key, descriptor) {
    const callback = descriptor.value;

    if (typeof callback !== 'function') {
      throw new SyntaxError('Only functions can be debounced');
    }

    var fn = _debounce(callback, wait, immediate)

    return {
      ...descriptor,
      value() {
        fn()
      }
    };
  }
}

3.debounce

때로는 실행 방법을 디바운스해야 합니다.

function time(prefix) {
  let count = 0;
  return function handleDescriptor(target, key, descriptor) {

    const fn = descriptor.value;

    if (prefix == null) {
      prefix = `${target.constructor.name}.${key}`;
    }

    if (typeof fn !== 'function') {
      throw new SyntaxError(`@time can only be used on functions, not: ${fn}`);
    }

    return {
      ...descriptor,
      value() {
        const label = `${prefix}-${count}`;
        count++;
        console.time(label);

        try {
          return fn.apply(this, arguments);
        } finally {
          console.timeEnd(label);
        }
      }
    }
  }
}

구현해 보겠습니다.

const SingerMixin = {
  sing(sound) {
    alert(sound);
  }
};

const FlyMixin = {
  // All types of property descriptors are supported
  get speed() {},
  fly() {},
  land() {}
};

@mixin(SingerMixin, FlyMixin)
class Bird {
  singMatingCall() {
    this.sing('tweet tweet');
  }
}

var bird = new Bird();
bird.singMatingCall();
// alerts "tweet tweet"

4 time

은 메서드를 계산하는 데 사용됩니다. 실행 시간:

function mixin(...mixins) {
  return target => {
    if (!mixins.length) {
      throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`);
    }

    for (let i = 0, l = mixins.length; i < l; i++) {
      const descs = Object.getOwnPropertyDescriptors(mixins[i]);
      const keys = Object.getOwnPropertyNames(descs);

      for (let j = 0, k = keys.length; j < k; j++) {
        const key = keys[j];

        if (!target.prototype.hasOwnProperty(key)) {
          Object.defineProperty(target.prototype, key, descs[key]);
        }
      }
    }
  };
}

5.mixin

은 클래스에 객체 메소드를 혼합하는 데 사용됩니다.

class MyReactComponent extends React.Component {}

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

mixin의 간단한 구현은 다음과 같습니다.

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

6.redux

실제 개발에서 React가 함께 사용되는 경우 Redux 라이브러리를 사용하면 다음과 같이 작성해야 하는 경우가 많습니다.

const method = descriptor.value;

데코레이터를 사용하면 위 코드를 다시 작성할 수 있습니다.

const value = descriptor.initializer && descriptor.initializer();

비교적으로 말하면 후자의 글쓰기 방식이 더 이해하기 쉬운 것 같아요.

7. 참고

위 내용은 모두 클래스 메소드를 수정하는 데 사용됩니다. 값을 얻는 방법은 다음과 같습니다.

rrreee

하지만 클래스의 인스턴스 속성을 수정하면 Babel을 통해 값을 얻을 수 없습니다. value 속성을 사용하면 다음과 같이 쓸 수 있습니다:

rrreee🎜🎜

위 내용은 ES6의 데코레이터에 대한 간략한 토론의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 segmentfault.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제