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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/46795955/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-29 06:55:31  来源:igfitidea点击:

How to know scroll to element is done in Javascript?

javascriptscrolljs-scrollintoview

提问by Sushant Gupta

I am using Javascript method Element.scrollIntoView()
https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView

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

Is there any way I can get to know when the scroll is over. Say there was an animation, or I have set {behavior: smooth}.

有什么办法可以让我知道滚动何时结束。假设有一个动画,或者我已经设置了{behavior: smooth}.

I am assuming scrolling is async and want to know if there is any callback like mechanism to it.

我假设滚动是异步的,想知道是否有任何类似机制的回调。

回答by guest271314

You can use IntersectionObserver, check if element .isIntersectingat IntersectionObservercallback function

您可以使用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"});
body {
  height: calc(100vh * 2);
}

#box {
  position: relative;
  top:500px;
}
<div id="box">
box
</div>

回答by niutech

There is no scrollEndevent, but you can listen for the scrollevent and check if it is still scrolling the window:

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

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

回答by Kaiido

For this "smooth" behavior, all the specssay is

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

When a user agent is to perform a smooth scroll of a scrolling box box to position, it must update the scroll position of box in a user-agent-defined fashionover a user-agent-defined amount of time.

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

(emphasizes mine)

(强调我的)

So not only is there no single event that will fire once it's completed, but we can't even assume any stabilized behavior between different browsers.

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

And indeed, current Firefox and Chrome already differ in their behavior:

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

  • Firefox seems to have a fixed duration set, and whatever the distance to scroll is, it will do it in this fixed duration ( ~500ms )
  • Chromeon the other hand will use a speed, that is, the duration of the operation will vary based on the distance to scroll, with an hard-limit of 3s.
  • Firefox 似乎有一个固定的持续时间设置,无论滚动距离是多少,它都会在这个固定的持续时间(~500ms)内完成
  • 另一方面,Chrome将使用速度,即操作的持续时间将根据滚动距离而有所不同,硬限制为 3 秒。

So this already disqualifies all the timeout based solutions for this problem.

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

Now, one of the answers here has proposed to use a MutationObserver, which is not a too bad solution, but which is not too portable, and doesn't take the inlineand blockoptions into account.

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

So the best might actually be to check regularly if we did stop scrolling. To do this in a non-invasive way, we can start an requestAnimationFramepowered loop, so that our checks are performed only once per frame.

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

Here one such implementation, which will return a Promise that will get resolved once the scroll operation has finished.
Note:This code misses a way to check if the operation succeeded, since if an other scroll operation happens on the page, all current ones are cancelled, but I'll let this as an exercise for the reader.

这是一个这样的实现,它将返回一个 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);
    }
  });
}
p {
  height: 400vh;
  width: 5px;
  background: repeat 0 0 / 5px 10px
    linear-gradient(to bottom, black 50%, white 50%);
}
body.scrolling {
  background: red;
}
<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>

回答by MajiD

i'm not an expert in javascript but i made this with jQuery. i hope it helps

我不是 javascript 专家,但我用 jQuery 做了这个。我希望它有帮助

$("#mybtn").click(function() {
    $('html, body').animate({
        scrollTop: $("div").offset().top
    }, 2000);
});

$( window ).scroll(function() {
  $("div").html("scrolling");
  if($(window).scrollTop() == $("div").offset().top) {
    $("div").html("Ended");
  }
})
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>

回答by Ryan Shillington

These answers above leave the event handler in place even after the scrolling is done (so that if the user scrolls, their method keeps getting called). They also don't notify you if there's no scrolling required. Here's a slightly better answer:

即使在滚动完成后,上面的这些答案也会保留事件处理程序(这样如果用户滚动,他们的方法就会继续被调用)。如果不需要滚动,他们也不会通知您。这是一个稍微好一点的答案:

$("#mybtn").click(function() {
    $('html, body').animate({
        scrollTop: $("div").offset().top
    }, 2000);

    $("div").html("Scrolling...");

    callWhenScrollCompleted(() => {
        $("div").html("Scrolling is completed!");
    });
});

// Wait for scrolling to stop.
function callWhenScrollCompleted(callback, checkTimeout = 200, parentElement = $(window)) {
  const scrollTimeoutFunction = () => {
    // Scrolling is complete
    parentElement.off("scroll");
    callback();
  };
  let scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);

  parentElement.on("scroll", () => {
    clearTimeout(scrollTimeout);
    scrollTimeout = setTimeout(scrollTimeoutFunction, checkTimeout);
  });
}
body { height: 2000px; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<button id="mybtn">Scroll to Text</button>
<br><br><br><br><br><br><br><br>
<div>example text</div>

回答by Krzysztof Podlaski

Solution that work for me with rxjs

使用 rxjs 对我有用的解决方案

lang: Typescript

语言:打字稿

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();
    }
  }

Usage:

用法:

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