我构建了一个拖放自动滚动器,用户可以将元素拖动到隐藏的 div
上。这会触发可滚动 div
的滚动操作。我正在使用scrollBy({top: <val>, behavior: 'smooth'}
获得平滑滚动和 requestAnimationFrame
以防止函数调用过于频繁。这在 Firefox 中运行良好,根据 caniuse 的说法,Chrome 应该原生支持该功能;但是,它在 Chrome 中无法正常工作。当用户离开隐藏状态时,它仅触发一次事件 div
。控制台中没有错误。 console.log()
表示包含 scrollBy()
的函数正在被调用。如果我删除 behavior: 'smooth'
它可以工作,但当然没有平滑滚动。如果我删除该选项并设置 css scroll-behavior: smooth
,结果相同在可滚动的 div 上。我完全不知所措。滚动函数的 MWE(这是在 Vue 应用程序中,因此任何 this.
都存储在数据对象中。
scroll: function () {
if ( this.autoScrollFn ) cancelAnimationFrame( this.autoScrollFn )
// this.toScroll is a reference to the HTMLElement
this.toScroll.scrollBy( {
top: 100,
behavior: 'smooth'
}
this.autoscrollFn = requestAnimationFrame( this.scroll )
}
最佳答案
不确定您期望 requestAnimationFrame
调用在此处执行什么操作,但以下是应该发生的情况:
scrollBy
将其行为设置为smooth
实际上应该仅在下一个绘画帧开始滚动目标元素,就在动画帧回调执行之前(step 7 here )。在平滑滚动的第一步之后,您的动画帧回调将触发 ( step 11 ),通过启动新的平滑滚动来禁用第一个平滑滚动 ( as defined here )。
重复直到达到最大顶部,因为您永远不会等待足够的时间来完全发生平滑的 100px 滚动。
这确实会在 Firefox 中移动,直到到达末尾,因为该浏览器具有线性平滑滚动行为并从第一帧开始滚动。
但Chrome有一个更复杂的ease-in-out行为,这将使第一次迭代滚动0px。因此,在这个浏览器中,您实际上会陷入无限循环,因为在每次迭代时,您都会滚动 0,然后禁用先前的滚动并再次要求滚动 0,等等。
const trigger = document.getElementById( 'trigger' );
const scroll_container = document.getElementById( 'scroll_container' );
let scrolled = 0;
trigger.onclick = (e) => startScroll();
function startScroll() {
// in Chome this will actually scroll by some amount in two painting frames
scroll_container.scrollBy( { top: 100, behavior: 'smooth' } );
// this will make our previous smooth scroll to be aborted (in all supporting browsers)
requestAnimationFrame( startScroll );
scroll_content.textContent = ++scrolled;
};
#scroll_container {
height: 50vh;
overflow: auto;
}
#scroll_content {
height: 5000vh;
background-image: linear-gradient(to bottom, red, green);
background-size: 100% 100px;
}
<button id="trigger">click to scroll</button>
<div id="scroll_container">
<div id="scroll_content"></div>
</div>
因此,如果您实际上想要避免多次调用该滚动函数,那么您的代码不仅在 Chrome 中会被破坏,而且在 Firefox 中也会被破坏(它也不会在 100px 之后停止滚动)。
在这种情况下,您需要的是等到平滑滚动结束。
已经有 a question here 关于检测平滑 scrollIntoPage
何时结束,但 scrollBy
情况有点不同(更简单)。
这里有一个方法,它将返回一个 Promise,让您知道平滑滚动何时结束(成功滚动到目的地时解析,并在被其他滚动中止时拒绝)。基本思想与 this answer of mine 相同:
启动 requestAnimationFrame 循环,在滚动的每一步检查是否到达静态位置。一旦我们将两帧保持在同一位置,我们就假设我们已经到达终点,然后我们只需要检查是否到达了预期位置。
有了这个,您只需升起一个标志,直到之前的平滑滚动结束,完成后,将其放下。
const trigger = document.getElementById( 'trigger' );
const scroll_container = document.getElementById( 'scroll_container' );
let scrolling = false; // a simple flag letting us know if we're already scrolling
trigger.onclick = (evt) => startScroll();
function startScroll() {
if( scrolling ) { // we are still processing a previous scroll request
console.log( 'blocked' );
return;
}
scrolling = true;
smoothScrollBy( scroll_container, { top: 100 } )
.catch( (err) => {
/*
here you can handle when the smooth-scroll
gets disabled by an other scrolling
*/
console.error( 'failed to scroll to target' );
} )
// all done, lower the flag
.then( () => scrolling = false );
};
/*
*
* Promised based scrollBy( { behavior: 'smooth' } )
* @param { Element } elem
** ::An Element on which we'll call scrollIntoView
* @param { object } [options]
** ::An optional scrollToOptions dictionary
* @return { Promise } (void)
** ::Resolves when the scrolling ends
*
*/
function smoothScrollBy( elem, options ) {
return new Promise( (resolve, reject) => {
if( !( elem instanceof Element ) ) {
throw new TypeError( 'Argument 1 must be an Element' );
}
let same = 0; // a counter
// pass the user defined options along with our default
const scrollOptions = Object.assign( {
behavior: 'smooth',
top: 0,
left: 0
}, options );
// last known scroll positions
let lastPos_top = elem.scrollTop;
let lastPos_left = elem.scrollLeft;
// expected final position
const maxScroll_top = elem.scrollHeight - elem.clientHeight;
const maxScroll_left = elem.scrollWidth - elem.clientWidth;
const targetPos_top = Math.max( 0, Math.min( maxScroll_top, Math.floor( lastPos_top + scrollOptions.top ) ) );
const targetPos_left = Math.max( 0, Math.min( maxScroll_left, Math.floor( lastPos_left + scrollOptions.left ) ) );
// let's begin
elem.scrollBy( scrollOptions );
requestAnimationFrame( check );
// this function will be called every painting frame
// for the duration of the smooth scroll operation
function check() {
// check our current position
const newPos_top = elem.scrollTop;
const newPos_left = elem.scrollLeft;
// we add a 1px margin to be safe
// (can happen with floating values + when reaching one end)
const at_destination = Math.abs( newPos_top - targetPos_top) <= 1 &&
Math.abs( newPos_left - targetPos_left ) <= 1;
// same as previous
if( newPos_top === lastPos_top &&
newPos_left === lastPos_left ) {
if( same ++ > 2 ) { // if it's more than two frames
if( at_destination ) {
return resolve();
}
return reject();
}
}
else {
same = 0; // reset our counter
// remember our current position
lastPos_top = newPos_top;
lastPos_left = newPos_left;
}
// check again next painting frame
requestAnimationFrame( check );
}
});
}
#scroll_container {
height: 50vh;
overflow: auto;
}
#scroll_content {
height: 5000vh;
background-image: linear-gradient(to bottom, red, green);
background-size: 100% 100px;
}
.as-console-wrapper {
max-height: calc( 50vh - 30px ) !important;
}
<button id="trigger">click to scroll (spam the click to test blocking feature)</button>
<div id="scroll_container">
<div id="scroll_content"></div>
</div>
关于javascript - Chrome 平滑滚动和 requestAnimationFrame?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59856814/