如何知道滚动到元素是在Javascript中完成的?

Sus*_*pta 21 javascript scroll js-scrollintoview

我使用的是Javascript方法Element.scrollIntoView()
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView

滚动结束时有什么办法可以让我知道.说有动画,或者我已经设定了{behavior: smooth}.

我假设滚动是异步的,并想知道是否有任何回调像它的机制.

Her*_*ine 13

我偶然发现了这个问题,因为我想在滚动完成后集中特定的输入(以便保持平滑滚动)。

如果您有与我相同的用例,您实际上不需要等待滚动完成来集中输入,您可以简单地禁用 focus 的滚动

其实现方式如下:

window.scrollTo({ top: 0, behavior: "smooth" });
myInput.focus({ preventScroll: true });
Run Code Online (Sandbox Code Playgroud)

比照: https: //github.com/w3c/csswg-drafts/issues/3744#issuecomment-685683932

顺便说一句,这个特定问题(在执行操作之前等待滚动完成)在 CSSWG GitHub 中讨论:https://github.com/w3c/csswg-drafts/issues/3744


gue*_*314 12

您可以使用IntersectionObserver,检查是否元素.isIntersectingIntersectionObserver回调函数

const element = document.getElementById("box");

const intersectionObserver = new IntersectionObserver((entries) => {
  let [entry] = entries;
  if (entry.isIntersecting) {
    setTimeout(() => alert(`${entry.target.id} is visible`), 100)
  }
});
// start observing
intersectionObserver.observe(box);

element.scrollIntoView({behavior: "smooth"});
Run Code Online (Sandbox Code Playgroud)
body {
  height: calc(100vh * 2);
}

#box {
  position: relative;
  top:500px;
}
Run Code Online (Sandbox Code Playgroud)
<div id="box">
box
</div>
Run Code Online (Sandbox Code Playgroud)

  • 很酷的功能。但在您的代码中同时使用“box”和“element”变量。有什么东西让人震惊.. (2认同)
  • 我想这只有在滚动之前“框”元素不可见时才有效。但它可能一直可见,并且 IntersectionObserver 将无法检测到它。 (2认同)

Kai*_*ido 12

对于这种“平滑”行为,所有规范都说的是

当用户代理是执行滚动盒框位置的平滑滚动,它必须更新框的滚动位置的用户代理定义的方式用户代理定义的时间量

(强调我的)

因此,不仅没有单个事件在完成后会触发,而且我们甚至无法假设不同浏览器之间的任何稳定行为。

事实上,当前的 Firefox 和 Chrome 的行为已经有所不同:

  • Firefox 似乎有一个固定的持续时间设置,无论滚动距离是多少,它都会在这个固定的持续时间(~500ms)内完成
  • 另一方面,Chrome将使用速度,即操作的持续时间将根据滚动距离而有所不同,硬限制为 3 秒。

因此,这已经取消了针对此问题的所有基于超时的解决方案的资格。

现在,这里的一个答案建议使用 MutationObserver,这不是一个太糟糕的解决方案,但它不太便携,并且没有考虑inlineblock选项。

所以最好的办法实际上可能是定期检查我们是否停止滚动。为了以非侵入性的方式做到这一点,我们可以启动一个requestAnimationFrame动力循环,这样我们的检查每帧只执行一次。

这里有一个这样的实现,它将返回一个 Promise,一旦滚动操作完成,该 Promise 将得到解决。
注意:这段代码遗漏了一种检查操作是否成功的方法,因为如果页面上发生了其他滚动操作,所有当前的滚动操作都会被取消,但我将把它作为读者的练习。

const buttons = [ ...document.querySelectorAll( 'button' ) ];

document.addEventListener( 'click', ({ target }) => {
  // handle delegated event
  target = target.closest('button');
  if( !target ) { return; }
  // find where to go next
  const next_index =  (buttons.indexOf(target) + 1) % buttons.length;
  const next_btn = buttons[next_index];
  const block_type = target.dataset.block;

  // make it red
  document.body.classList.add( 'scrolling' );
  
  smoothScroll( next_btn, { block: block_type })
    .then( () => {
      // remove the red
      document.body.classList.remove( 'scrolling' );
    } )
});


/* 
 *
 * Promised based scrollIntoView( { behavior: 'smooth' } )
 * @param { Element } elem
 **  ::An Element on which we'll call scrollIntoView
 * @param { object } [options]
 **  ::An optional scrollIntoViewOptions dictionary
 * @return { Promise } (void)
 **  ::Resolves when the scrolling ends
 *
 */
function smoothScroll( elem, options ) {
  return new Promise( (resolve) => {
    if( !( elem instanceof Element ) ) {
      throw new TypeError( 'Argument 1 must be an Element' );
    }
    let same = 0; // a counter
    let lastPos = null; // last known Y position
    // pass the user defined options along with our default
    const scrollOptions = Object.assign( { behavior: 'smooth' }, options );

    // let's begin
    elem.scrollIntoView( 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 = elem.getBoundingClientRect().top;
      
      if( newPos === lastPos ) { // same as previous
        if(same ++ > 2) { // if it's more than two frames
/* @todo: verify it succeeded
 * if(isAtCorrectPosition(elem, options) {
 *   resolve();
 * } else {
 *   reject();
 * }
 * return;
 */
          return resolve(); // we've come to an halt
        }
      }
      else {
        same = 0; // reset our counter
        lastPos = newPos; // remember our current position
      }
      // check again next painting frame
      requestAnimationFrame(check);
    }
  });
}
Run Code Online (Sandbox Code Playgroud)
p {
  height: 400vh;
  width: 5px;
  background: repeat 0 0 / 5px 10px
    linear-gradient(to bottom, black 50%, white 50%);
}
body.scrolling {
  background: red;
}
Run Code Online (Sandbox Code Playgroud)
<button data-block="center">scroll to next button <code>block:center</code></button>
<p></p>
<button data-block="start">scroll to next button <code>block:start</code></button>
<p></p>
<button data-block="nearest">scroll to next button <code>block:nearest</code></button>
<p></p>
<button>scroll to top</button>
Run Code Online (Sandbox Code Playgroud)

  • [caniuse/scrollend_event](https://caniuse.com/mdn-api_element_scrollend_event) 目前为 2.3%。 (3认同)
  • @Sagivb.g IIRC 我添加了这个愚蠢的检查,因为我经历过一些浏览器正在等待全帧才能开始滚动,并且从非动画文档调用 rAF 实际上不会等待下一帧。所以碰巧剧本在动画真正开始之前就已经看到它已经结束了。但是,是的,这是一个神奇的数字。 (2认同)

niu*_*ech 7

没有scrollEnd事件,但是您可以侦听scroll事件并检查事件是否仍在滚动窗口:

var scrollTimeout;
addEventListener('scroll', function(e) {
    clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(function() {
        console.log('Scroll ended');
    }, 100);
});
Run Code Online (Sandbox Code Playgroud)

  • 我花了一些时间来了解这里到底发生了什么。对于其他对此感到困惑的人,这里的解决方案假设滚动事件在滚动发生时比每 100 毫秒触发一次更频繁,因此如果我们有一个 100 毫秒的窗口,其中没有进一步的滚动事件触发,那么这一定是滚动操作的结束。看起来效果很好,并且符合 MDN 建议,不要在滚动事件中执行任何计算复杂的操作。 (4认同)
  • 谢谢!也许我会在滚动端添加一个“removeEventListener” (4认同)
  • 这对我来说非常有效。我的“scrollIntoView()”触发了一个不相关的滚动函数,因此我在scrollIntoView之前删除了侦听器,但我需要一种方法来知道滚动何时完成以重新添加侦听器。好处是,在此处使用命名函数添加事件侦听器可以防止重复添加它。 (2认同)

小智 5

适用于我的 rxjs 解决方案

语言:打字稿

scrollToElementRef(
    element: HTMLElement,
    options?: ScrollIntoViewOptions,
    emitFinish = false,
  ): void | Promise<boolean> {
    element.scrollIntoView(options);
    if (emitFinish) {
      return fromEvent(window, 'scroll')
        .pipe(debounceTime(100), first(), mapTo(true)).toPromise();
    }
  }
Run Code Online (Sandbox Code Playgroud)

用法:

const element = document.getElementById('ELEM_ID');
scrollToElementRef(elment, {behavior: 'smooth'}, true).then(() => {
  // scroll finished do something
})
Run Code Online (Sandbox Code Playgroud)