Ладно, парни и девчонки, вот вам, волшебники, хитрый вопрос...
Я работаю над иммерсивным веб-приложением, которое переопределяет поведение сенсорной прокрутки по умолчанию на мобильных устройствах. Контент разделен на страницы, которые используют 100% области просмотра, а навигация осуществляется свайпом вверх и вниз между страницы.
При первом свайпе я вызываю requestFullscreen() элемент body, что, конечно же, вызывает перекомпоновку при изменении размера области просмотра. Проблема в том, что я также хочу, чтобы это первое смахивание запускало пользовательское поведение прокрутки, но я использую Element.nextElementSibling.scrollIntoView({ block : start, behavior : 'smooth' }), и пока перекомпоновка не будет завершена, верхний край следующей страницы (HTMLSectionElement) уже виден, поэтому прокрутка не происходит.
Если я использую setTimeout, чтобы подождать около 600 мс, пока не завершится перекомпоновка, эффект прокрутки будет работать, как и ожидалось, но мне не нравится этот хакерский обходной путь, и я бы предпочел использовать более элегантное асинхронное решение.
Сначала я попытался вызвать эффект прокрутки изнутри исполнителя resolve обещания, возвращаемого requestFullscreen, но это не помогло. Это обещание разрешается очень рано в потоке выполнения.
Затем я попытался изнутри обработчика событий fullscreenchange. Здесь тоже не повезло, так как это событие запускается немедленно до, происходит полноэкранное изменение.
Наконец, я попытался изнутри обработчика событий окна resize, но это срабатывает до того, как произойдет перекомпоновка. Я тоже добавил requestIdleCallback здесь, но это не имело никакого значения.
Итак, мой вопрос... Есть ли надежный способ определить конец операции перекомпоновки? Или, альтернативно... есть ли у кого-нибудь лучший план Б, чем отказ от использования scrollIntoView и кодирование моего собственного эффекта прокрутки в обработчик изменения размера окна.
Я спросил об этом почти 2 года назад о конкретной проблеме с веб-приложением, над которым я работал в то время. К сожалению, Fullscreen API запросы не работают внутри iframe элементов, поэтому добавить работающий фрагмент задачи было невозможно. Конкретная проблема, которая у меня была 2 года назад, в любом случае больше не имеет значения, поэтому я, вероятно, перепишу это, чтобы сделать его более общим и полезным вопросом для более широкой аудитории.



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


В случае, если кто-то хочет знать, я справился с этой ситуацией, запустив эффект прокрутки из прослушивателя событий изменения размера окна через прокси-функцию опровергать. Он по-прежнему использует setTimeout, но постоянно сбрасывает счетчик, чтобы гарантировать, что прокрутка произойдет после запуска всех событий изменения размера (4 в случае Chrome).
Это не самое элегантное решение, и эффект прокрутки может быть отложен больше, чем это абсолютно необходимо, но, по крайней мере, прокрутка никогда не будет заблокирована перекомпоновкой, и я могу с этим смириться.
Надеюсь, однажды у нас будет событие ReflowEndEvent или что-то подобное, о котором мы сможем послушать.
Хорошо, будущие гуглеры, я вернулся пару лет спустя с ответом НАСТОЯЩИЙ, не хакерский на эту проблему.
Хитрость заключается в использовании двухэтапной цепочки requestAnimationFrame внутри ResizeObserver. Первый обратный вызов запускается прямо перед тем, как происходит перекомпоновка. Вы можете использовать этот обратный вызов для внесения любых изменений в DOM в последнюю секунду. Затем внутри этого обратного вызова мы снова используем requestAnimationFrame, чтобы указать другой обратный вызов, который произойдет после рисования из предыдущего кадра.
Вы можете проверить синхронизацию этого метода, раскомментировав строки debugger; в приведенном ниже примере кода.
<!DOCTYPE html>
<html lang = "en">
<head>
<meta charset = "utf-8">
<meta name = "description" content = "React to element size changes after layout reflow has occured.">
<title> Reflow Observer Test </title>
<style>
* { box-sizing: border-box; }
html, body { height: 100vh; margin: 0; padding: 0; }
body { background: grey; }
body:fullscreen { background: orange;}
body:fullscreen::backdrop { background: transparent; }
#toggle { margin: 1rem; }
</style>
</head>
<body>
<button id = "toggle"> Toggle Fullscreen </button>
<script>
let resizableNode = document.body;
resizableNode.addEventListener('reflow', event => {
console.info(event.timeStamp, 'do stuff after reflow here');
});
let observer = new ResizeObserver(entries => {
for (let entry of entries) {
requestAnimationFrame(timestamp => {
// console.info(timestamp, 'about to reflow'); debugger;
requestAnimationFrame(timestamp => {
// console.info(timestamp, 'reflow complete'); debugger;
entry.target?.dispatchEvent(new CustomEvent('reflow'));
});
});
}
});
observer.observe(resizableNode);
function toggleFullscreen(element) {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
element.requestFullscreen();
}
}
document.getElementById('toggle').addEventListener('click', event => {
toggleFullscreen(resizableNode);
});
</script>
</body>
</html>
В этом примере я использую переход в полноэкранный режим для элемента body в качестве триггера, но это можно легко применить к любой операции изменения размера на любом узле DOM.
Я обернул это в хороший аккуратный класс ES6, доступный здесь github.com/besworks/ReflowObserver
Зачем снова ждать полный кадр? Просто подождите одну задачу или, что еще лучше, используйте requestPostAnimationFrame, где это возможно, см. этот мой ответ. Также имейте в виду, что все браузеры не запускают авто-reflow одновременно. Например, Safari вызовет его в режиме ожидания еще до того, как сработает rAF, поэтому в этом браузере, если вы ошибетесь с CSSOM в rAF, вы фактически вызовете новую, ненужную перекомпоновку прямо перед отрисовкой.
Отличные заметки @kaiido, я не слышал о requestPostAnimationFrame. Это действительно работает вместо второго requestAnimationFrame, когда я тестировал в Chrome 89. В моем случае меня беспокоят только браузеры на основе Chromium, но ваши заметки о времени в других браузерах могут помочь другим сузить круг их проблем.
Да, и если вы ориентируетесь только на браузеры Chromium, будьте осторожны, их requestAnimationFrame реализация — полностью сломан. Если вы не вызываете rAF из анимированного документа, вы можете вообще не оказаться перед следующей отрисовкой.
Очень помогло, спасибо!
Операция переплавка является синхронной. Чего может не быть, так это когда его вызывают. См. этот мой ответ. Итак, нет, на самом деле вы не ищете «обнаружить конец операции перекомпоновки». Я не уверен, что вы ищете, потому что в вашем вопросе отсутствует минимальный воспроизводимый пример. Может быть, вы скорее хотите «форсировать» перекомпоновку, чтобы CSSOM была актуальной, когда вы начинаете прокрутку, или вы хотите определить, когда изменение размера заканчивается? Но вы задаете не правильный вопрос.