-
Notifications
You must be signed in to change notification settings - Fork 15
Жизненный цикл компонента. Декораторы @hook и @wait
У обычного компонента V4 есть стандартный жизненный цикл: компонент создан, компонент вставлен в DOM, компонент удален и т. д. V4 реализует расширенный вариант жизненного цикла компонента Vue. Т. е. V4 компонент поддерживает все состояния жизненного цикла (далее хуки) компонента Vue и добавляет 4 своих:
- beforeRuntime — хук, который вызывается до
beforeCreate
; - beforeDataCreate — хук, который вызывается после
beforeCreate
, но доcreated
; - beforeMounted — хук, который вызывается после
beforeMount
, но доmounted
(используется для внутренних нужд); - beforeUpdated — хук, который вызывается после
beforeUpdate
, но доupdated
(используется для внутренних нужд).
Необходимость данного хука существует из-за ограничений 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
.
Часто бывает нужно внести некоторые модификации наблюдаемых свойств (например, нормализацию) до того, как компонент будет создан (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
бывает весьма полезен.
Чтобы привязать метод к определенному хуку существует 2 способа:
- Для всех 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);
}
}
- Можно использовать специальный декоратор
@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, то остальные будут ожидать его выполнения, чтобы порядок инициализации сохранялся.
У всех компонентов 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
.
У всех наследников iBlock есть специальный метод waitStatus
, который первым параметром получает имя ожидаемого статуса и возвращает Promise, когда компонент перейдет в этот статус (если компонент уже в этом статусе, то обещание зарезолвится сразу). Возвращаемый объект упакован в Async, поэтому вторым параметром метод может получать объект настроек:
{
join?: boolean | 'replace';
label?: string | symbol;
group?: string;
}
Также, у метода waitStatus
есть перегрузка, когда он вторым параметром получает функцию (объекты настроек можно передать третьим параметром). Если компонент уже находится в этом статусе, то функция немедленно выполнится, а метод вернет ее результат (не обернутый в Promise). В противном же случае вернется Promise, который также будет содержать результат функции. У этой перегрузки объект параметров может принимать дополнительную опцию defer: true
, которая говорит, что даже если компонент уже находится в этом статусе, то метод все равно вернет Promise, а функция выполнится отложено. С этой опцией важно использовать label
.
Многие методы компонента требуют, чтобы для их работы компонент находился в определенном статусе. Мы могли бы использовать метод 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);
}
}