Как стать автором
Обновить

JavaScript декораторы наконец-то в Stage 3

Время на прочтение5 мин
Количество просмотров7.2K

18 апреля 2022 года, после 5 лет доработки (первый коммит от 30 апреля 2017 года), proposal по декораторам наконец-то достиг 3 стадии, что означает что у него есть спецификация, тестовая имплементация и осталась только полировка на основе фидбека от разработчиков. Учитывая что это уже четвертая (!) итерация декораторов, их переход в стадию принятия это эпохальное событие для JS - не припомню ни одной другой фичи, которая прошла такой длинный и тернистый путь, с диаметрально разными подходами и аж двумя разными legacy-имплементациями, в Babel и TypeScript. Давайте же посмотрим на неё повнимательней.

Ссылки

Репозиторий самого предложения, включая все предыдущие версии (в истории коммитов).

История предложений, включая ссылки на все четыре основные версии.

Независимая имплементация.

Плагин для Babel.

Кстати, новая версия датируется в Babel как 2021-12 - потому что была представлена на саммите TC39 в декабре 2021 года.

Чем отличается от предыдущих версий

Во-первых, новые декораторы пока работают только с классами и их элементами. Впрочем, предложения по расширению той же логики на функции/параметры/объекты/переменные/аннотации/блоки/инициализаторы есть, но в текущую спеку не входят (что неудивительно, вряд ли кто-то хочет потратить еще 5 лет на достижение Stage 4).

Во-вторых, главное отличие новых декораторов: они работают только с сущностью которую декорируют (класс, поле класса, метод, геттер/сеттер и аксессор - новая сущность, о которой далее), а не с дескрипторами свойств и/или прототипами классов, как легаси подходы.

То есть они не способны добавить новые сущности в прототип/инстанс класса или хотя бы изменить их вид (с поля на геттер/сеттер, например), а могут только преобразовать ту сущность, которая описана в исходном коде - обернуть её в дополнительную логику или полностью заменить на другую, но того же вида.

Это было сделано в первую очередь под давлением разработчиков V8 основных движков, так как чрезмерная гибкость предыдущих декораторов крайне плохо подходила для оптимизации кода в рантайме - именно поэтому принятие декораторов так затянулось.

Демо и синтаксис применения декораторов

Ну и сразу полный пример со всеми возможными комбинациями синтаксиса:

//export должен быть перед декоратором
export default
//декоратор класса, может изменять сам класс
@defineElement("some-element")
class SomeElement extends HTMLElement {
  //декоратор поля - может заменить значение поля при инициализации класса
  //все дальнейшие чтения/записи он не отслеживает
  @inject('some-dep')
  dep

  //новый синтаксис - аксессор
  //по факту просто сахар для пары геттер/сеттер
  //похож на автоматически реализуемые свойства в C#
  //могут быть и приватными и статическими
  //декоратор может отслеживать чтение/запись
  @reactive accessor clicked = false

  //ну с методами и прочим все как обычно
  @logged
  someMethod() {
    return 42
  }

  //да, с приватными элементами тоже работает, как и со статическими
  //название декоратора может быть через точку
  @random.int(0, 42)
  #val

  @logged
  get val() {
    return this.#val
  }

  @logged
  set val(value) {
    this.#val = value
  }

  //апофеоз:
  //статический приватный аксессор c декоратором со сложным доступом
  @(someArr[3].someFunc('param'))
  static acсessor #name = 'some-element'
}

Текущие имплементации полифиллов этот пример полностью переварить еще не могут но, думаю, в скором времени это исправится.

Синтаксис для применения декораторов в целом не слишком отличается от привычного, есть только пара деталей:

  1. Декоратор класса должен идти после export (если он есть) - это наверное главное отличие от статус-кво.

  2. Для "обычного" применения декоратора можно использовать идентификатор, точку и вызов функции - @dotted.form.with('some-call')

  3. Для "сложного" применения можно использовать синтаксис со скобками: @(complex[1])

Написание декораторов

Тут никаких особых сюрпризов - декоратор это обычная функция с таким типом:

context предоставляет, как ни странно, контекст, сведения о месте применения декоратора, где:

  • kind - вид элемента, на который применяется декоратор;

  • name - название элемента;

  • access - объект который позволяет в произвольный момент времени получить/установить значение элемента, может пригодиться, например, для DI. Разрешен только для элементов класса, но не для самих классов (то есть get или set есть только когда kind != 'class');

  • private и static - есть ли у элемента класса соответствующие модификаторы;

  • addInitializer позволяет выполнить код после того как сам класс (не инстанс!) или элемент класса полностью определен - например, в нем можно зарегистрировать класс в DI или забиндить метод. Не применим только для поля класса (то есть определен когда kind != 'field' - об этом далее).

Input и Output зависят от kind, но в целом Input - это значение элемента как оно написано в коде, а Output - значение на которое оно будет заменено в рантайме.

Важный нюанс - для полей класса (когда kind == 'field') Input всегда undefined, а Output может быть функцией вида (initValue: unknown) => any - эта функция вызывается при инициализации класса для вычисления начального значения поля. Именно из-за этого для поля класса не передается addInitializer - Output его заменяет.

Пример декоратора logged:

function logged(value, { kind, name }) {
  if (kind === "method") {
    return function (...args) {
      console.log(`starting ${name} with arguments ${args.join(", ")}`);
      const ret = value.call(this, ...args);
      console.log(`ending ${name}`);
      return ret;
    };
  }
  if (kind === "field") {
    return function (initialValue) {
      console.log(`initializing ${name} with value ${initialValue}`);
      return initialValue;
    };
  }
  if (kind === "class") {
    return class extends value {
      constructor(...args) {
        super(...args);
        console.log(`constructing an instance of ${name} with arguments ${args.join(", ")}`);
      }
    }
}

Ну или вот customElement с использованием addInitializer:

function customElement(name) {
  return (value, { addInitializer }) => {
    addInitializer(function() {
      customElements.define(name, this);
    });
  }
}

@customElement('my-element')
class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ['some', 'attrs'];
  }
}

Больше примеров (в том числе и с применением access для DI) смотрите на гитхабе.

Аксессоры

Ограничения новых декораторов в виде запрета на изменение вида элемента в целом логичны, но они убивают один крайне важный юзкейс декораторов - когда поле превращается в пару геттер/сеттер с дополнительной логикой вокруг. Это может быть, например, логгирование изменений поля для отладки, а может быть полноценная система реактивности, как в MobX, который, по сути, основан на этом хаке:

import {computed, observable, autorun} from 'mobx'

class Counter {
  	//вот здесь поле превращается в геттер/сеттер
    @observable num = 1
  	//а будет так
    @observable accessor num = 1
    
    @computed
    get double() {
        return this.num * 2
    }
}

const counter = new Counter()

//выведет 2
autorun(() => console.log(counter.double)) 

//когда изменяем num, изменится и double
counter.num = 2
//autorun выполняется снова и выводит 4

С новыми декораторами все такие поля придется помечать как accessor что, конечно, не слишком весело, но в целом терпимо и может отслеживаться, например, тайпскриптом. Под капотом работать это будет примерно так:

class C {
  accessor x = 1;
}

//Раскрывается в...

class C {
  #x = 1;

  get x() {
    return this.#x;
  }

  set x(val) {
    this.#x = val;
  }
}

Имплементации

Пока ждем реализации в основных тулзах - в первую очередь это, конечно, поддержка аксессоров как нового синтаксиса. Когда IDE, TypeScript и Babel (esbuild и т.д.) смогут их корректно обрабатывать, сделать полифиллы будет не так и сложно.

И я крайне надеюсь что TypeScript будет корректно обрабатывать типы декораторов при замене значений - сейчас декоратор никак не может повлиять на тип декорируемого значения.

Ссылки для отслеживания внедрения:

TypeScript - фича включена в планы на версию 4.8.

esbuild - ждут реализации в TS/node/браузерах.

Ну а потом последует волна переезда на новую реализацию со стороны экосистемы. К счастью, декораторы в JS не так и распространены, и при этом новые декораторы могут быть реализованы в библиотеках вместе со старыми - их сигнатура отличается от Babel/TS декораторов.

Дождались, в общем.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Пользуетесь декораторами в JS?
33.77% Да!51
66.23% Нет…100
Проголосовал 151 пользователь. Воздержались 19 пользователей.
Теги:
Хабы:
Всего голосов 10: ↑10 и ↓0+10
Комментарии21

Публикации

Истории

Работа

Ближайшие события