如何检测以 javascript 结束的窗口滚动?

mid*_*ack 1 html javascript scroll

我想在滚动结束后向选定元素添加一个类。JS如何检测滚动结束?

超文本标记语言

<ul class="list" id="wrapper">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li id="element">7</li>
    <li>8</li>
    <li>9</li>
    <li>10</li>
 </ul>
Run Code Online (Sandbox Code Playgroud)

JS

const element = document.getElementById('element');
const y = element.getBoundingClientRect().top + window.scrollY;


window.scroll({
  top: y,
  behavior: 'smooth'
});
Run Code Online (Sandbox Code Playgroud)

JSBIN 示例

Kai*_*ido 7

2023 年更新:

现在有一个scrollend活动您可以收听。当滚动结束时,无论是用户滚动还是编程平滑滚动,这都会触发。浏览器支持还不是很好,但应该很快就会在所有地方可用。

if (!("onscrollend" in window)) {
  console.warn("Your browser doesn't support the onscrollend event");
}

const trigger = document.getElementById( 'trigger' );
const scroll_forcer = document.getElementById( 'scroll_forcer' );

let scrolling = false; // a simple flag letting us know if we're already scrolling
trigger.onclick = (evt) => startScroll();

function startScroll() {
  setTimeout(()=> {
    scroll_forcer.classList.add( "scrolling" )
    document.scrollingElement.scrollTo( { top: 1000, behavior: "smooth" } );
    document.addEventListener( "scrollend", (evt) => {
      scroll_forcer.classList.remove( "scrolling" );
    }, { once: true });
    }, 10);
};
Run Code Online (Sandbox Code Playgroud)
#scroll_forcer {
  height: 5000vh;
  background-image: linear-gradient(to bottom, red, green);
  background-size: 100% 100px;
}
#scroll_forcer.scrolling {
  filter: grayscale(70%);
}
Run Code Online (Sandbox Code Playgroud)
<button id="trigger">click to scroll</button>
<div id="scroll_forcer">
</div>
Run Code Online (Sandbox Code Playgroud)

所以暂时我可能还需要之前的答案:


本身没有事件可以告诉平滑滚动何时结束。

对于这种平滑滚动应该如何发生也没有标准行为,没有定义的持续时间,没有定义的计时函数(Chrome 使用缓入输出函数,而 Firefox 使用线性函数),并且添加了平滑滚动可以通过对滚动算法的其他调用在中间取消,甚至根本没有效果......

所以这个检测并不是那么容易的。

我目前发现的最好方法是启动一个requestAnimationFrame驱动的循环,如果我们的目标位于同一位置,它将检查每个绘画帧(在滚动操作之后)。一旦它在同一位置超过两帧,我们就假设滚动结束。这时我们可以检查它是否成功,只需检查我们是否处于预期位置即可:

const trigger = document.getElementById( 'trigger' );
const scroll_forcer = document.getElementById( 'scroll_forcer' );

let scrolling = false; // a simple flag letting us know if we're already scrolling
trigger.onclick = (evt) => startScroll();

function startScroll() {
  setTimeout(()=> {
    scroll_forcer.classList.add( "scrolling" )
    smoothScrollTo( { top: 1000 } )
      .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( () => scroll_forcer.classList.remove( "scrolling" ) );
    }, 10);
};


/* 
 *
 * Promised based window.scrollTo( { 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 smoothScrollTo( options ) {
  return new Promise( (resolve, reject) => {
    const elem = document.scrollingElement;
    let same = 0; // a counter

    // last known scroll positions
    let lastPos_top = elem.scrollTop;
    let lastPos_left = elem.scrollLeft;

    // pass the user defined options along with our default
    const scrollOptions = Object.assign( {
        behavior: 'smooth',
        top: lastPos_top,
        left: lastPos_left
      }, options );

    // 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, scrollOptions.top ) );
    const targetPos_left = Math.max( 0, Math.min( maxScroll_left, scrollOptions.left ) );

    // let's begin
    window.scrollTo( 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 );
    }
  });
}
Run Code Online (Sandbox Code Playgroud)
#scroll_forcer {
  height: 5000vh;
  background-image: linear-gradient(to bottom, red, green);
  background-size: 100% 100px;
}
#scroll_forcer.scrolling {
  filter: grayscale(70%);
}
.as-console-wrapper {
  max-height: calc( 50vh - 30px ) !important;
}
Run Code Online (Sandbox Code Playgroud)
<button id="trigger">click to scroll</button>
<div id="scroll_forcer">
</div>
Run Code Online (Sandbox Code Playgroud)