Активация Mutation Observer при быстром скролле на мобильниках Андроид

Всем привет! Уже запустили сайт, сейчас дорабатываем - Музыка – Чароўныя нажнiчкi
На этой странице есть слайдер. Реализован с помощью 3D Materializecss библиотеки и Mutation Observer API.

Механизм работы такой: при перелистывании названий мелодий в карусели, когда она останавливается, идёт fetch БД и в разметку вставляется соответствующее этой мелодии описание. Случайно обнаружил баг: если просматривать эту страницу на мобильнике (НЕ на эмуляции мобильного на десктопе) Андроид, то при относительно быстром скролле описания под слайдером вдруг происходит его перезагрузка, хотя страница не перезагружается, делегирования событий нет, но вновь открывается то же самое описание, что было, т.е. сам слайдер не меняет ничего. Может, кто-то сталкивался с таким и знает, что делать?

Вот код работы на этой странице:


import preload from '../js/preloader.mjs';
import {
  carouselOptions,
  initCarousel,
} from '../js/materializeCarousel.mjs';

const URL = ".........."; // он правильный
const carousel = document.querySelector(".music__list.carousel");
const melody = document.querySelector(".music__melody-passport");

function createPath(url, substring) {
  return url + substring;
}

async function fetchMelodyPassport(url) {
  const response = await fetch(url);
  const melody_passport_json = await response.json();
  return melody_passport_json.content.rendered;
}

async function onScrollingChange() {
  const melodies_ids = [];

  if (!carousel.classList.contains("scrolling")) {
    melodies_ids.push(carousel.querySelector(".carousel-item.active").dataset.melody_passport_id);
    const melody_id = melodies_ids.pop();

    const url = createPath(URL, melody_id);
    const melodyPassport = await fetchMelodyPassport(url);

    melody.innerHTML = melodyPassport;
    preload();

    return;
  }

  melody.textContent = "";
  preload();
}

const instance = initCarousel(carousel, carouselOptions);
const observer = new MutationObserver(onScrollingChange);
observer.observe(carousel, { attributes: true });

P.S. Как я понимаю, Mutation Observer (т.е. по его срабатыванию у меня происходит Fetch) почему-то активируется на быстрый скролл на мобильниках…

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

Если я угадал, то избежать мигания можно запоминая какой слайд карусели был последним активным, и внутри включается mustation observer не включать запрос если слайд по завершению анимации не сменился.

1 лайк

Привет! Случайно удалось воспроизвести баг на десктопе: если расположить toolbar Chrome снизу и подвигать им вверх-вниз (т.е. мы исключаем горизонтальное смещение), это активирует слайдер, хотя внешне ничего не меняется, он не вращается и даже не дёргается), и запускается анимация (((( Блин, это нужно тогда лезть в библиотеку его, чтобы там что-то поменять…

Звучит как реакция на ресайз.

В чем проблема дописать кода и не реагировать на подобные ситуации? (как я выше предлагал не делать запросы если слайд по факту не сменился)

Пока не могу сообразить, как его поменять. Логика ясна. А вот что конкретно и куда - пока не понимаю. Уточнил - при навешивании класса “scrolling” на контейнер карусели класс active у активного слайда не меняется. Попробую, чтобы Mutation observer следил за активным слайдом, а не за появлением scrolling на контейнере…

@dmitry , удалось реализовать твою идею, спасибо - оказалось самой простой ) Но возникла трудность в другом: слайдер работает в связке с прелоадером. Если сравнивать старый и новый слайды, то это происходит уже после остановки карусели, когда активный слайд определился. Получается, что пока карусель крутится, прелоадера нет. Мне кажется, это нелогично: как бы то не было, всё же смена кадра подразумевается в результате вращения. А прелоадер появится только в самом конце, когда вращение уже остановится и будет выполняться fetch. И снова я попадаю в ту ловушку ложного срабатывания на ресайз, когда истинного вращения нет, а прелоадер сработает.

Как ты верно подметил “если предположить что смена кадра заканчивается загрузкой нового слайда”. Но в реальности оказывается что смена кадра может начаться и не завершится сменой одного на другой.

Ща мы с тобой тут накрутим…

Есть вариант не показывать прелоадер сразу в момент начала смены слайда, а планировать показ после некоторого таймаута после начала движения слайдера. Через 100-300ms, с точными цифрами надо “поиграться”. И отменять этот таймаут если движение завершилось а слайд не сменился. Внутри моей головы кажется так ты получишь желаемый результат:

  • на случайных ресайзах прелоадер не будет виден потому что таймаут отменится
  • при перелистывании карусели прелоадер начнет появляться до появления следующего слайда

Ну а цена всей этой точности и красоты - сложность кода и повышенные шансы багов типа того что карусель проехала, а данные не подгрузились.

Ох, обожаю крутить )) Давай! Глядишь, шо и накрутим полезного. Ну, а честно, какой-то сюр: задача кажется элементарной, но уже вторые сутки уже голову сломал ((
Как смотришь на такую идею: подвязаться к ресайзу body? Про ресайзе боди вешать на карусель какой-нибудь класс, типа resized. Если каруселя вдруг очнулась, проверять, есть ли у нее ещё и класс resized. Если есть - значит, это ложная активация и ничего не делаем. Если есть только ее собственный активный и только - Урра!! Закрываем всё прелоадером и начинаем кружиться.
P.S. Блин, всё новые идеи приходят, когда комп далеко, чтобы опробировать их ((

Бывает стоит подумать еще.

Где-то есть решение неожиданное, рабочее и без тонны лишнего кода. Накрайняк, если хорошая идея не прийдет, всегда можно написать много кода.

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

Я нашел в библиотеке карусели строку с window.addEventListener(‘resize’, блаблабла). Попробовал за комментировать - баг уходит. Вроде, больше ничего изменилось. Ну, кроме того, что при горизонтальном ресайзе она больше не подстраивается под ширину контейнера. Может, и хрен с ней? Я так понимаю, ресайз - это когда тягаешь окно в панели разработчика. А так при загрузке страницы карусель в любом случае рассчитывается правильно. Будет ли кто из пользователей тягать это окно в панели… Или ресайз - это не только, когда тягаешь окно в панели?

Любое действие которое меняет размер body элемента. А это: разворачивание-сворачивание на весь экран, поворот сайта горизонтально-вертикально на мобильнике, изменение размера окна когда тягаешь за краешек окна. Может еще что упустил.

Может попробовать поиграться и добавить throttle или еще какие условия в тело библиотеки?

А вот идея запустить ручки в библиотеку тоже по-своему валидная если не найдем решения, правда у нее есть цена (не обновиться так легко, неожиданные баги, поддержка проекта со временем). Я так делал несколько раз, просто перенося-копируя код библиотеки в проект. Иногда делая форк и используя его (так удобнее поддерживать-обновляться со внеменем).

Пока что не звучит как достойное применения решение.

Самое интересное, что throttle уже есть в библиотеке. У меня ощущение, что самое надежное решение через всё тот же MutationObserver. Там можно установить наблюдение за изменением и детей. Когда карусель реально крутится, то возникает изменение инлайновых стилей на элементах. Но дело вот в чем: эта функция, которую observer вызывает, вызывается столько раз, сколько будет мутаций. Раз слайдов 10 - будет 10 раз коллбэк вызван. И в нем как раз функции по fetch и preloader. И карусель начинает биться в конвульсиях. Я не понимаю, как сделать так, чтобы отсечь эту кучу вызовов коллбэка и запустить fetch и preload только раз, когда коллбэк прекратит запускаться. Я знаю про async-await, но эта функция запускается многократно…

Библиотека эта старенькая, она давно не поддерживается, но, как ни странно, на ES6 она может инициализироваться. Это единственное, что мне попалось как 3d карусель бесплатная. Очень легко получилось ее подключить и она в остальном отлично работает

Ну, и самый крайний вариант - сделать 2 слайдера: один с названиями, другой с соответствующими описаниями и их синхронизировать. Загрузить с начала всё, что нужно, через цикл WP, дождаться, а потом уже ничего грузить не нужно будет, слайды просто будут мотаться вместе с их названиями.

throttle


Еще вариант, если уж полезли в кишки библиотеки: стрелять события изнутри самой библиотеки и уже подвешиваться к ним для нужной реакции извне


Мне не нравятся решения которые мы тут обсуждаем потому что они слишком окольные, сложные и неочевидные читающему. Где-то тут прячется простое решение. Нужно активно подумать сегодня и потом на свежую голову завтра. Мозг сделает работу в фоне если его сильно загрузить а потом дать отдохнуть.

Согласен. Извини, пожалуйста! Я предлагаю вообще этот вопрос и ветку удалить - здесь всё какое-то неясно - не соответствующе причине бага. И ещё раз спасибо за идеи и общение!

По-первых если решение кому-то не нравится это не значит что его не надо реализовывать (особенно если оно решает задачу). Мои чувства - это чуйка, интуиция. Она не всегда результативна и соответствует действительности.

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

В-третьих обсуждение действительно вышло за пределы формата “вопрос-ответ”. Топик легко можно сделать типа “обсуждение” если нет никакого из сообщений которое ты готов пометить как ответ на твой вопрос.

Пиши что думаешь - я сделаю соответственно.

Еле-еле, но решение нашёл. Вот такой получился код:

import preloader from '../js/preloader.mjs';

import {
  carouselOptions,
  initCarousel,
} from '../js/materializeCarousel.mjs';

const URL = "..................." // он правильный :);
const carousel = document.querySelector(".music__list.carousel");


// новый блок
let isCarouselClicked = false;
let isCarouselDragged = false;
const melody = document.querySelector(".music__melody-passport");
// конец нового блока


function createPath(url, substring) {
  return url + substring;
}

async function fetchMelodyPassport(url) {
  const response = await fetch(url);
  const melody_passport_json = await response.json();
  return melody_passport_json.content.rendered;
}


// бывшая часть коллбэка mutationobserver вынесена в отдельную функцию
async function createMelodyPassport() { // запрашивает по id активного слайда соответствующее описание и вставляет в разметку, после чего убирает прелоадер
  const melody_id = carousel.querySelector(".carousel-item.active").dataset.melody_passport_id;
  const url = createPath(URL, melody_id);
  const melodyPassport = await fetchMelodyPassport(url);

  melody.innerHTML = melodyPassport;
  preloader.hidePreloader();
}
// конец бывшей части коллбэка mutationobserver, вынесенной в отдельную функцию

// обновлённый коллбэк mutationobserver
function onScrollingChange() {
  const isCarouselRunning = carousel.classList.contains("scrolling");
  isCarouselDragged = instance.dragged;

  if (!isCarouselRunning) {
    createMelodyPassport();
    if (isCarouselClicked) isCarouselClicked = !isCarouselClicked;
    if (isCarouselDragged) instance.dragged = false;
    return;
  }

  if (isCarouselClicked || isCarouselDragged) {
    preloader.showPreloader();
  }
}
// конец обновлённого коллбэка mutationobserver


const instance = initCarousel(carousel, carouselOptions);

// новый блок
instance.el.addEventListener("click", () => isCarouselClicked = true);
// конец нового блока


const observer = new MutationObserver(onScrollingChange);
observer.observe(carousel, { attributes: true });

Поясню: удалось откопать в доке этой карусели, что у неё (instance.el) есть свойство dragged, которое true, когда карусель свайпнули. А вот свойства clicked нет (есть какое-то странное, когда слайд зажали мышью и при этом тянут). В связи с этим я накинул addEventListener на instance.el - по клику.

Идея была такова: карусель начнёт вращаться тогда, когда на ней произойдёт какое-нибудь событие (клик или свайп). Если нет - значит, это был ресайз. В общей зоне видимости создал 2 переменные: 1 - для клика (isCarouselClicked) и 2-ю - для свайпа (isCarouselDragged). MutationOberver-ру осталась та же функция.

Ну, а дальше, если карусель завертелась, значит, isCarouselClicked или isCarouselDragged станут true - они поставят прелоадер на время вращения карусели. Когда она остановится (т.е. исчезнет класс scrolling), MutationObserver ещё раз вызовет свой коллбэк, выполнится условие “карусель остановилась”, что вызовет createMelodyPassport. В конце булевы переменные снова превратятся в false и будут готовы для нового цикла работы карусели.

1 лайк