Skip to content

Жизненный цикл компонента. Декораторы @hook и @wait

Andrey Kobets edited this page Jul 4, 2019 · 11 revisions

У обычного компонента V4 есть стандартный жизненный цикл: компонент создан, компонент вставлен в DOM, компонент удален и т. д. V4 реализует расширенный вариант жизненного цикла компонента Vue. Т. е. V4 компонент поддерживает все состояния жизненного цикла (далее хуки) компонента Vue и добавляет 4 своих:

  • beforeRuntime — хук, который вызывается до beforeCreate;
  • beforeDataCreate — хук, который вызывается после beforeCreate, но до created;
  • beforeMounted — хук, который вызывается после beforeMount, но до mounted (используется для внутренних нужд);
  • beforeUpdated — хук, который вызывается после beforeUpdate, но до updated (используется для внутренних нужд).

beforeRuntime

Необходимость данного хука существует из-за ограничений Vue: дело в том, что когда компонент вызывается в шаблоне, то у него есть состояние, когда у него еще нет своих методов и свойств, а только входные параметры (beforeCreate). После beforeCreate у компонента вызывается специальная функция, которая формирует базовый объект с наблюдаемыми свойствами компонента и уже потом срабатывает created. Так вот, до created мы не можем использовать API компонента: вызывать его методы, геттеры и т. д. Однако для поддержки некоторых методов в базовом классе iBlock существует следующий код:

@hook('beforeRuntime')
protected initBaseAPI() {
  const
    i = this.instance;

  this.syncStorageState = i.syncStorageState.bind(this);
  this.syncRouterState = i.syncRouterState.bind(this);

  this.watch = i.watch.bind(this);
  this.on = i.on.bind(this);
  this.once = i.once.bind(this);
  this.off = i.off.bind(this);
}

Т. е. до beforeCreate срабатывает специальный метод, который явно устанавливает самый необходимый API, который всегда должен быть у компонента. Количество методов, которые могут использоваться до created не так много, и они как правило все перечислены в iBlock, но если в вашем компоненте появится такой метод, то всегда можно переопределить initBaseAPI.

beforeDataCreate

Часто бывает нужно внести некоторые модификации наблюдаемых свойств (например, нормализацию) до того, как компонент будет создан (created), т. к. после создания любое изменение такого свойство вызовет перерендер и может иметь катастрофические последствия для производительности. У нас есть ссылки, функции инициализаторы и механизмы управления порядком инициализации, но что, если нам нужно получить весь наблюдаемый объект и модифицировать его комплексно. Именно для решения этой проблемы существует хук beforeDataCreate: он вызовется именно тогда, когда все наблюдаемые свойства созданы, но еще не прилинкованы к компоненту, т. е. мы сможем их безопасно изменить и не ожидать последствий.

import iBlock, { component, field, hook } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';

@component()
export default class bExample extends iBlock {
  @field()
  i: number = 0;

  @field()
  j: number = 0;

  @hook('beforeDataCreate')
  normalizeData() {
    // Т. к. @field свойства еще не прилинкованы к объекту,
    // то мы не можем их вызывать напрямую, а только через специальные методы
    if (this.getField('i') === 0) {
      this.setField('j', 1);
    }
  }
}

Следует также отметить, что @prop и @system свойства инициализируется на beforeCreate, поэтому для доступа к ним не нужны специальные методы или хуки.

Как правило, для создания связей при инициализации и нормализации лучше использовать механизмы ссылок, но тем ни менее, beforeDataCreate бывает весьма полезен.

Добавление слушателей на хук. Декоратор @hook

Чтобы привязать метод к определенному хуку существует 2 способа:

  1. Для всех Vue совместимых хуков можно определить одноименный метод, который будет автоматически линковаться с хуком:
import iBlock, { component, field } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';

@component()
export default class bExample extends iBlock {
  @field()
  i: number = 0;

  // Вызовется на хук created
  created() {
    console.log(this.i);
  }
}
  1. Можно использовать специальный декоратор @hook, который принимает название хука или массив названий.
import iBlock, { component, field, hook } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';

@component()
export default class bExample extends iBlock {
  @field()
  i: number = 0;

  @hook(['created', 'mounted'])
  logI() {
    console.log(this.i);
  }
}

Второй способ является предпочтительным, т. к. позволяет писать более гибкий код. Обратите внимание, что «не стандартные» хуки beforeRuntime и beforeDataCreate можно использовать только через декоратор.

Порядок выполнения хука

Все обработчики хуков выполняются в рамках очереди: сначала выполняются добавленные через декоратор (в порядке добавления), а затем уже выполняется ассоциированный метод (если таковой имеется). Если нам нужно сказать, что некоторый метод должен выполнится только после выполнения другого, то мы можем задать это явно через декоратор:

import iBlock, { component, field, hook } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';

@component()
export default class bExample extends iBlock {
  @hook('created')
  a() {
    console.log('a');
  }

  @hook(['created', 'mounted'])
  b() {
    console.log('b');
  }

  @hook({created: 'b'})
  c() {
    console.log('c');
  }

  @hook({created: ['a', 'b'], mounted: 'b'})
  d() {
    console.log('d');
  }
}

Асинхронные обработчики

Некоторые хуки поддерживают асинхронные обработчики: mounted, updated, destroyed, errorCaptured. Т. е. если один из хуков вернет Promise, то остальные будут ожидать его выполнения, чтобы порядок инициализации сохранялся.

Свойство hook

У всех компонентов V4 есть свойство hook, которое показывает, в каком хуке сейчас находится компонент.

Статус компонента

Помимо хуков жизненного цикла у компонентов V4 есть другое, «параллельное» множество более абстрактных состояний:

  • unloaded — компонент не загружен (beforeRuntime, beforeCreate);
  • loading — компонент загружается (beforeDataCreate, activated и каждый раз, когда компонент переинициализируется);
  • beforeReady — компонент готов к полной загрузке: всегда возникает после loading;
  • ready — компонент полностью загружен и вставлен в DOM;
  • inactive — компонент находится в не активном состоянии, но не уничтожен (deactivated);
  • destroyed — компонент уничтожен (beforeDestroy).

Как видите, многие из этих статусов пересекаются с хуками, но это пересечение не является целью статусов. Что является показателем, что компонент полностью загружен? Может быть mounted? Хук mounted говорит, что компонент успешно создан и вставлен в DOM, но при этом он может грузить данные с сервера или локального хранилища, показывая индикатор загрузки, а уже после получения всех необходимых данных снова перерисоваться. Или скажем, от сервера по сокету пришло событие, что компонент должен обновиться с новыми данными, так какой хук может это обозначать? Как раз для ответа на этот вопрос и существуют статусы: компонент грузит данные — loading; полностью загружен — ready и т. д.

Как же добавить обработчик на тот или иной статус? Декоратор @hook в данном случае работать не будет, т. к. статусы не являются хуками, однако, изменения статуса всегда порождает событие status-название, поэтому мы можем использовать декоратор @watch:

import iBlock, { component, field, watch } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';

@component()
export default class bExample extends iBlock {
  @field()
  i: number = 0;

  // Мы можем использовать camelCase,
  // т.к. название события будет нормализовано
  @watch(':statusReady')
  logI() {
    console.log(this.i);
  }
}

Также, у всех наследников iBlock есть специальный геттер isReady, который равен true, когда компонент в статусе ready.

Метод waitStatus

У всех наследников iBlock есть специальный метод waitStatus, который первым параметром получает имя ожидаемого статуса и возвращает Promise, когда компонент перейдет в этот статус (если компонент уже в этом статусе, то обещание зарезолвится сразу). Возвращаемый объект упакован в Async, поэтому вторым параметром метод может получать объект настроек:

{
  join?: boolean | 'replace';
  label?: string | symbol;
  group?: string;
}

Также, у метода waitStatus есть перегрузка, когда он вторым параметром получает функцию (объекты настроек можно передать третьим параметром). Если компонент уже находится в этом статусе, то функция немедленно выполнится, а метод вернет ее результат (не обернутый в Promise). В противном же случае вернется Promise, который также будет содержать результат функции. У этой перегрузки объект параметров может принимать дополнительную опцию defer: true, которая говорит, что даже если компонент уже находится в этом статусе, то метод все равно вернет Promise, а функция выполнится отложено. С этой опцией важно использовать label.

Декоратор @wait

Многие методы компонента требуют, чтобы для их работы компонент находился в определенном статусе. Мы могли бы использовать метод waitStatus для решения этой задачи:

import iBlock, { component, field } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';

@component()
export default class bExample extends iBlock {
  @field()
  i: number = 2;

  getIPow(num: number): number | Promise<number> {
    return this.waitStatus('ready', () => Math.pow(this.i, num));
  }
}

Однако, есть более удобный способ с помощью декоратора:

import iBlock, { component, field } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';

@component()
export default class bExample extends iBlock {
  @field()
  i: number = 2;

  @wait('ready')
  getIPow(num: number): number | Promise<number> {
    return Math.pow(this.i, num);
  }
}

Декоратор @wait поддерживает все параметры, что и перегруженная форма метода waitStatus.

import iBlock, { component, field } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';

@component()
export default class bExample extends iBlock {
  @field()
  i: number = 2;

  @wait('ready', {defer: true, label: 'getIPow'})
  getIPow(num: number): Promise<number> {
    return Math.pow(this.i, num);
  }
}

Следует отметить, что @wait и waitStatus по умолчанию устаналивают join: true, если у переданной функции нет аргументов, и join: 'replace' — если они есть.

Отложенные методы

Декоратор @wait имеет перегрузку, когда название ожидаемого статуса опускается, т. е. нам не важно какой статус у компонента, когда вызывается этот метод. Какой тогда толк от всего этого? Дело в том, что мы по прежнему можем передать объект дополнительных настроек и свойство defer: true. Таким образом мы сделаем наш метод «ленивым» без необходимость ожидать тот или иной статус, однако не стоит забывать про label при таком подходе, иначе вызовы не будут «схлопываться».

import iBlock, { component, field } from 'super/i-block/i-block';
export * from 'super/i-block/i-block';

@component()
export default class bExample extends iBlock {
  @field()
  i: number = 2;

  @wait({defer: true, label: 'getIPow'})
  getIPow(num: number): Promise<number> {
    return Math.pow(this.i, num);
  }
}
Clone this wiki locally