Медиа-запросы в JS выполняются правильно
У каждого может возникнуть ситуация, связанная с прослушиванием ширины экрана приложения.
Существует множество методов достижения этого, но хотелось бы чего-то простого, многоразового и универсального, что не слишком влияло бы на производительность.
Прослушивание события resize
в window
и проверка innerWidth
происходит слишком медленно, тем более что все, что связано с макетом, вызывает перепрошивку.
Во время поиска решений рассмотрим API window.matchMedia(), и представим, что это лучшее решение (оно также поддерживается во всех основных браузерах).
Единственное, что останется сделать, - это внедрить простое, многоразовое и универсальное решение.
Нам хотелось бы иметь возможность subscribe
на различные медиа-запросы из разных компонентов приложения, не создавая новых слушателей.
Решение состоит в том, чтобы создать static class
(class
со static
методами и переменными), который сохранял бы список запросов и подписок для этих запросов.
Этот класс будет предоставлять 2 общедоступных метода: subscribe
и unsubscribe
.
Метод subscribe
принимает функцию обратного вызова, которая получает boolean
параметр, представляющий, соответствует ли запрос или нет, и фактический запрос в строковом формате (например: '(max-width: 768px)'
).
Затем он проверяет, есть ли уже подписка, созданная для этого запроса из какого-либо другого места:
- если это так, то он только добавляет обратный вызов в список подписчиков для этого запроса.
- если это не так, он добавляет запрос в список, причем единственным подписчиком является переданный обратный вызов, затем он начинает прослушивать изменения для вновь созданного запроса.
В любом случае, он также мгновенно вызывает обратный вызов с текущим совпадением запроса.
export class MediaQueryListener {
//...
static subscribe = (cb: (matchesQuery: boolean) => void, query: string) => {
if (!this._subscribers[query]) {
this._subscribers[query] = {
query: window.matchMedia(query),
subscribers: [ cb ],
};
// keep the refs so we can remove the listeners in unsubscribe
this._eventListenersRefs[query] = event => {
this._subscribers[query]
.subscribers
.forEach(subscriber => subscriber(event.matches));
};
// create the listener only on first subscription for the query
this._subscribers[query]
.query
.addEventListener('change', this._eventListenersRefs[query]);
} else {
this._subscribers[query].subscribers.push(cb);
}
// Call the callback with the current value of the media query
cb(window.matchMedia(query).matches);
};
}
Метод unsubscribe
получает ту же функцию обратного вызова и запрос в строковом формате и удаляет обратный вызов у подписчиков этого запроса. Он также проверяет, не осталось ли больше подписчиков, и удаляет прослушиватель медиа-запросов, чтобы не осталось зависших прослушивателей.
export class MediaQueryListener {
//...
static unsubscribe = (cb: (matchesQuery: boolean) => void, query: string) => {
if (!this._subscribers[query]) {
return;
}
this._subscribers[query].subscribers = this._subscribers[query].subscribers.filter(subscriber => subscriber !== cb);
if (this._subscribers[query].subscribers.length === 0) {
this._subscribers[query].query.removeEventListener('change', this._eventListenersRefs[query]);
delete this._subscribers[query];
delete this._eventListenersRefs[query];
}
};
}
Это базовая оболочка поверх matchMedia
api. Вероятно, существует несколько пакетов, которые так или иначе достигают одной и той же цели.