javascript - Chrome 平滑滚动和 requestAnimationFrame?

标签 javascript google-chrome smooth-scrolling requestanimationframe

我构建了一个拖放自动滚动器,用户可以将元素拖动到隐藏的 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/

相关文章:

CSS,无法在 chrome 中对齐 div

javascript - 使 JavaScript 图像在滚动时移动更流畅

c# - 滚动时在 Panel 中平滑绘制或绘制子控件

javascript - 将 <i> 放入 <li> 底部自动调整高度

javascript - Chrome 扩展 - 弹出窗口中的 onMessage 未收到来自注入(inject)脚本的消息

javascript - 在 accounting.js 中使用 money.js 的问题

javascript - jQuery If 语句仅适用于平滑滚动

javascript - Highcharts 显示空白

jquery - 嵌入 pdf chrome 并不总是有效

google-chrome - 在 Chrome 中打印 Google Apps 脚本模板