Heim  >  Artikel  >  Web-Frontend  >  Eine kurze Diskussion über Dekorateure in ES6

Eine kurze Diskussion über Dekorateure in ES6

不言
不言nach vorne
2018-11-15 17:35:142483Durchsuche

In diesem Artikel geht es darum, kurz über Dekorateure in ES6 zu sprechen. Ich hoffe, dass er für Freunde in Not hilfreich ist.

Dekorateur

Dekorator wird hauptsächlich verwendet für:

  1. Dekorationsklasse

  2. Dekorationsmethode oder -attribut

Dekorationsklasse

@annotation
class MyClass { }

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

Dekorationsmethode oder -attribut

class MyClass {
  @readonly
  method() { }
}

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

Babel

Installation und Kompilierung

Das können wir machen es in Babel Probieren Sie es auf der offiziellen Website aus, um den von Babel kompilierten Code anzuzeigen.

Wir können uns jedoch auch für die lokale Kompilierung entscheiden:

npm init

npm install --save-dev @babel/core @babel/cli

npm install --save-dev @babel/plugin-proposal-decorators @babel/plugin-proposal-class-properties

Erstellen Sie eine neue .babelrc-Datei

{
  "plugins": [
    ["@babel/plugin-proposal-decorators", { "legacy": true }],
    ["@babel/plugin-proposal-class-properties", {"loose": true}]
  ]
}

Kompilieren Sie die angegebene Datei

babel decorator.js --out-file decorator-compiled.js

Kompilieren Sie die Dekoration Klasse

Vor der Kompilierung:

@annotation
class MyClass { }

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

Nach der Kompilierung:

var _class;

let MyClass = annotation(_class = class MyClass {}) || _class;

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

Wir können die Dekoration von Klassen sehen, das Prinzip ist:

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

Zusammenstellung der Dekorationsmethoden

Vor der Kompilierung:

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

Nach der Kompilierung:

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

Kompilierte Quellcode-Analyse der Dekorationsmethode

Wir können sehen, dass Babel eine _applyDecoratedDescriptor-Funktion für erstellt hat Dekorieren Sie die Methode.

Object.getOwnPropertyDescriptor()

Bei der Übergabe von Parametern verwenden wir eine Object.getOwnPropertyDescriptor()-Methode:

Object Der getOwnPropertyDescriptor ()-Methode gibt den Eigenschaftsdeskriptor zurück, der einer eigenen Eigenschaft für das angegebene Objekt entspricht. (Eigene Eigenschaften beziehen sich auf Eigenschaften, die dem Objekt direkt zugewiesen sind und nicht in der Prototypenkette nachgeschlagen werden müssen)

Beachten Sie übrigens, dass es sich hierbei um eine ES5-Methode handelt.

Zum Beispiel:

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,
// }

Der erste Teil der Quellcode-Analyse

Innerhalb der Funktion _applyDecoratedDescriptor erstellen wir zunächst das von Object.getOwnPropertyDescriptor() zurückgegebene Eigenschaftsdeskriptorobjekt. Kopieren:

// 拷贝一份 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;
}

Was ist also das Initialisierungsattribut? Das von Object.getOwnPropertyDescriptor() zurückgegebene Objekt verfügt tatsächlich nicht über diese Eigenschaft, die von der Babel-Klasse generiert wird, um beispielsweise mit dem folgenden Code zusammenzuarbeiten:

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 Kompiliert als:

// ...
(_descriptor = _applyDecoratedDescriptor(_class.prototype, "born", [readonly], {
    configurable: true,
    enumerable: true,
    writable: true,
    initializer: function() {
        return Date.now();
    }
}))
// ...

Zu diesem Zeitpunkt verfügt der an die Funktion _applyDecoratedDescriptor übergebene Deskriptor über das Initialisierungsattribut.

Der zweite Teil der Quellcode-Analyse

Der nächste Schritt besteht darin, mehrere Dekoratoren anzuwenden:

/**
 * 第二部分
 * @type {[type]}
 */
desc = decorators
    .slice()
    .reverse()
    .reduce(function(desc, decorator) {
        return decorator(target, property, desc) || desc;
    }, desc);

Für eine Methode werden mehrere Dekoratoren angewendet, wie zum Beispiel:

class MyClass {
  @unenumerable
  @readonly
  method() { }
}

Babel wird kompiliert zu:

_applyDecoratedDescriptor(
    _class.prototype,
    "method",
    [unenumerable, readonly],
    Object.getOwnPropertyDescriptor(_class.prototype, "method"),
    _class.prototype
)

Im zweiten Teil des Quellcodes werden die Operationen reverse() und Reduce() ausgeführt. Daraus können wir auch schließen, dass dieselbe Methode mehrere hat Dekorateure werden von innen nach außen ausgeführt.

Teil 3 Quellcode-Analyse

/**
 * 第三部分
 * 设置要 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;

Wenn desc ein Initialisierungsattribut hat, bedeutet dies, dass beim Dekorieren des Klassenattributs der Wert von value auf Folgendes gesetzt wird:

desc.initializer.call(context)

Der Wert des Kontexts ist _class.prototype. Der Grund für call(context) ist ebenfalls leicht zu verstehen, da es

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

  getNum() {
    return 1;
  }
}

ist. Letztendlich wird es so sein ausgeführt werden:

Object["define" + "Property"](target, property, desc);

Es ist ersichtlich, dass die Dekorationsmethode im Wesentlichen mit Object.defineProperty() implementiert wird.

Anwendung

1.log

Fügen Sie die Protokollfunktion zu einer Methode hinzu und überprüfen Sie die Eingabeparameter:

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

Perfekter:

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

2.autobind

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

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

getPerson() === person;
// true

Ein Szenario, das wir uns leicht vorstellen können, ist, wenn React Ereignisse bindet:

class Toggle extends React.Component {

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

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

Schreiben wir eine solche Autobind-Funktion:

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

3 .debounce

Manchmal müssen wir die ausgeführte Methode entprellen:

class Toggle extends React.Component {

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

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

Lassen Sie es uns implementieren:

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

4.time

Wird zum Zählen der Methodenausführungszeit verwendet:

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

5.mixin

Wird verwendet, um Objektmethoden in die Klasse zu mischen:

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"

Eine einfache Implementierung von mixin ist wie folgt:

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

6.redux

Wenn React in der tatsächlichen Entwicklung in Verbindung mit der Redux-Bibliothek verwendet wird, muss es häufig wie folgt geschrieben werden.

class MyReactComponent extends React.Component {}

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

Mit dem Decorator können Sie den obigen Code umschreiben.

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

Relativ gesehen scheint die letztere Schreibweise leichter zu verstehen.

7. Hinweis

Die oben genannten Methoden werden alle zum Ändern der Klassenmethoden verwendet:

const method = descriptor.value;

Aber wenn wir das Instanzattribut der Klasse ändern Aufgrund von Babel kann der Wert nicht über das Wertattribut abgerufen werden. Wir können ihn wie folgt schreiben:

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

Das obige ist der detaillierte Inhalt vonEine kurze Diskussion über Dekorateure in ES6. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen