好的,伙计们,对于你们所有巫师来说,这是一个棘手的问题......
我正在开发一个沉浸式 Web 应用程序,该应用程序覆盖移动设备上的默认触摸滚动行为。内容被划分为使用 100% 视口的页面,并通过在页面之间上下滑动来处理导航。
在第一次滑动时,我在body
元素上调用requestFullscreen()
,这当然会导致视口大小调整时重排。问题是我还希望第一次滑动以触发自定义滚动行为,但我使用的是Element.nextElementSibling.scrollIntoView({ block : start, behavior : 'smooth' })
,直到重排完成,下一页(HTMLSectionElement
)的上边缘已经可见,因此滚动不会发生。
如果我使用setTimeout
等待大约 600 毫秒直到重排完成,滚动效果会按预期工作,但我对这种笨拙的解决方法不满意,我更喜欢使用更优雅的异步解决方案。
我首先尝试从requestFullscreen返回的Promise的resolve
执行器内部触发滚动效果,但这没有帮助。此承诺在执行流的早期就解决了。
然后我尝试从fullscreenchange
事件处理程序内部。这里也没有运气,因为此事件是在全屏更改发生之前立即触发的。
最后,我尝试从窗口内部resize
事件处理程序,但这在回流发生之前触发。我也在这里添加了一个requestIdleCallback
,但它没有任何区别。
所以我的问题是...有没有可靠的方法来检测回流操作的结束?或者...有没有人有比放弃使用scrollIntoView
并将我自己的滚动效果编码到窗口大小调整处理程序中更好的 B 计划。
好的,未来的谷歌员工,几年后我回来了,对这个问题有一个真实的、非黑客的答案。
诀窍是在ResizeObserver
内使用两步requestAnimationFrame
链。第一个回调在回流发生之前触发。您可以使用此回调对 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.log(event.timeStamp, 'do stuff after reflow here');
});
let observer = new ResizeObserver(entries => {
for (let entry of entries) {
requestAnimationFrame(timestamp => {
// console.log(timestamp, 'about to reflow'); debugger;
requestAnimationFrame(timestamp => {
// console.log(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 节点上的任何调整大小操作。
如果有人想知道,我通过去抖动代理函数从窗口调整大小事件侦听器触发滚动效果来处理这种情况。它仍然使用setTimeout
但会不断重置计数器,以确保在所有调整大小事件触发后进行滚动(Chrome 为 4)。
这不是最优雅的解决方案,滚动效果可能会延迟超过绝对必要的时间,但至少滚动永远不会被回流阻塞,我可以忍受。
希望有一天我们会有一个ReflowEndEvent
事件或类似的事情,我们可以听。