При разработке приложений часто возникает необходимость получить какие-то данные с сервера и показать их на странице. Обычно, это делается примерно так:
{{#if data}}
//Отображаем данные
{{else}}
//Отображаем информацию о том, что идет загрузка, например, preloader.
{{/if}}
Когда загружать данные? Конечно, чем раньше, тем лучше. Для этого идеально подходит событие init у страницы. Почему не mounted? Потому, что в общем случае, оно не подходит: при использовании keepAlive оно будет вызываться и при перемещении страницы из памяти в DOM, а в этом случае данные уже все есть и загружать ничего не нужно.
С событием понятно, далее следует простой механизм:
- Получили данные.
- Вызвали $setState(), чтобы показать данные пользователю.
События страницы асинхронные, это значит что $setState может быть вызван в одном из двух случаев:
- Идет “показ” страницы, т. е. анимация, т.е. afterIn еще не вызвано.
- Страница уже показана, анимация завершена, событие afterIn вызвано.
В какой из двух моментов будет вызван $setState, зависит от того, как быстро придет от сервера ответ. На моей практике чаще встречаются сервера, которые отвечают быстро и получается случай 1. Для проектов, где я пишу backend, всегда возникает случай 1.
Тут и возникает проблема: в итоге вы обновляем DOM когда идет анимация страницы и видим тормоза (даже на простом интерфейсе и даже на iPhone), что говорить о “сложных” страницах.
Что делать? Конечно, делать изменения в DOM только после того, как анимация завершена, а для этого есть специальное событие afterIn. Какие варианты приходят в голову?
Первый, самый простой, - это просто загружать данные в событии afterIn. Но тут есть сразу 2 проблемы:
- Мы упускаем драгоценное время, ведь в большинстве случаев мы уже готовы показать пользователю данные в afterIn, а тут мы только начнем их загружать.
- afterIn будет вызываться и тогда, когда страница просто помещается во View, а создана при этом давно.
Если 2ю проблему можно обойти, заведя отдельную переменную с информацией о том, загружены ли данные, то 1ю проблему - не решить.
Вариант второй - использовать событие beforeIn, тут мы имеем такие же проблемы, хотя потерянное время будет меньше (это про проблему 1).
Вариант третий, правильный - нужно загружать данные в init, а показывать их только в afterIn. Для этого идеально подойдет Promise. Для начала напишем глобальную Mixin:
Framework7.registerComponentMixin('update-lock', {
data: function() {
return {
updateLockPromise : null, //Сам Promise, он нам нужен в компоненте роутера
updateLockPromiseResolve : null, //Ссылка на resolve в Promise, по другому вызывать нельзя
}
},
methods : {
createUpdateLock() { //Создаем блокировку, т.е. Promise
this.updateLockPromise = new Promise((resolve => {
this.updateLockPromiseResolve = resolve; //Сохраняем ссылку на resolve, чтобы потом его вызвать
}));
},
destroyUpdateLock() { //Удаляем блокировку, просто вызываем resolve для Provise
this.updateLockPromiseResolve();
}
},
on : {
pageInit() {
this.createUpdateLock(); // Ставим блокировку
},
pageAfterIn() {
this.destroyUpdateLock(); //Снимаем блокировку
}
}
});
В самом компоненте роутера нам нужно подключить Mixin:
mixins: [ 'update-lock']
Затем нам нужно не показывать данные, пока не снята блокировка, для этого есть 2 варианта,
async () => {
await this.updateLockPromise; //Ждем, дальше не идем
/* Далее код работает с $setState */
}
или так (что одно и тоже)
this.updateLockPromise.then(() => {
/* Далее код работает с $setState */
})
Что же будет, если Promise уже будет помеченный как resolve, когда данные придут с сервера? Он сразу выполнится и все отработает правильно.
Теперь у нас больше нет “лаг” при изменении в DOM, если они совпадают с анимацией страницы.
Дополнение 1
Сразу возникает идея, вообще не вызывать $setState, если сейчас идет анимация страницы. Например, на странице мы подписываемся на событие и по его приходу что-то обновляем в DOM. Но на практике, конечно, этот случай крайне редкий.
У нас есть еще одно событие reinit: возможно стоит добавить еще одну установку блокировки? К сожалению, на мой взгляд, это событие вызывается не тогда когда нужно, поэтому при его наступлении устанавливать блокировку нельзя.
Посмотрите примеры:
page1 > page2 > (back) page1 (reinit - логично).
page1 > page2 > page3 >(back) page2 (init page1 - логично, нужно для Swipe Back) > (back) page1 (reinit - не логично, для нее уже вызван init)
Тогда можно пойти по другому пути: завести другой Promise и ставить блокировку на beforeIn, а снимать при afterIn.
Дополнение 2
Если данные придут быстро с сервера, то preloader быстро покажется и скроется. Это некрасиво, желательно установить какое-то минимальное время показа, скажем 800 мс.
В Mixin добавим отдельную переменную:
updateLockDestroyDelay: 0
А в компоненте роутера мы ее можем переопределить. Теперь необходимо изменить pageAfterIn в Mixin:
pageAfterIn() {
setTimeout(() => {
if (typeof this.destroyUpdateLock !== 'undefined') { //Компонента может уже не и быть, например мы ушли раньше, чем закончился таймаут
this.destroyUpdateLock();
}
}, this.updateLockDestroyDelay);
}
Вот и все, надеюсь, будет полезно.