scroll 滚动事件节流、requestAnimationFrame、性能优化

本文探讨了如何利用requestAnimationFrame进行滚动事件的性能优化,详细解释了requestAnimationFrame的工作原理及其优于setTimeout的地方。同时,介绍了使用passive属性改善移动端滚屏性能,以减少页面卡顿,提升用户体验。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、scroll 滚动事件

由于滚动事件可以高速触发,因此事件处理程序不应执行计算成本高的操作,例如 DOM 修改。 相反,建议使用 requestAnimationFrame()setTimeout()CustomEvent 来优化事件,如下所示。

  // 使用 requestAnimationFrame 节流
  let lastKnownScrollPosition = 0;
  let ticking = false;

  function doSomething(scrollPosition) {
    // Do something with the scroll position
  }

  document.addEventListener(
    "scroll",
    function (e) {
      lastKnownScrollPosition = window.scrollY;

      if (!ticking) {
        // 使用 requestAnimationFrame 节流
        window.requestAnimationFrame(function () {
          doSomething(lastKnownScrollPosition);
          ticking = false;
        });

        ticking = true;
      }
    },
    {
      // 使用 passive 可改善的滚屏性能
      passive: true,
    }
  );

注意, input events 和 animation frames(动画帧)以大致相同的速率触发,因此通常不需要上面面的优化。


二、使用 requestAnimationFrame 节流

1. 屏幕刷新频率

图像在屏幕上更新的速度(屏幕上的图像每秒钟出现的次数),它的单位是赫兹(Hz)。

60Hz:显示器也会以每秒60次的频率正在不断的更新屏幕上的图像,每次的间隔时间是 16.7ms(1000/60≈16.7) 。

2. 动画原理

动画本质就是要让人眼看到图像被刷新而引起变化的视觉效果,这个变化要以连贯的、平滑的方式进行过渡。

3. setTimeout

利用seTimeout实现的动画在某些低端机上会出现卡顿、抖动的现象。 原因:

  • setTimeout的执行时间并不是确定的。在Javascript中, setTimeout 任务被放进了异步队列中,只有当主线程上的任务执行完以后,才会去检查该队列里的任务是否需要开始执行,因此 setTimeout 的实际执行时间一般要比其设定的时间晚一些。
  • 刷新频率受屏幕分辨率和屏幕尺寸的影响,因此不同设备的屏幕刷新频率可能会不同,而 setTimeout只能设置一个固定的时间间隔,这个时间不一定和屏幕的刷新时间相同。

4. requestAnimationFrame

  • requestAnimationFrame 充分利用显示器的刷新机制,由系统来决定回调函数的执行时机,从而节省系统资源,提高系统性能,改善视觉效果。
  • requestAnimationFrame 的步伐跟着系统的刷新步伐走。它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
  • requestAnimationFrame 告诉浏览器希望执行一个动画,让浏览器在下一个动画帧安排一次网页重绘。
  • requestAnimationFrame 会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。
  • 显示器有固定的刷新频率(60Hz或75Hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想就是与这个刷新频率保持同步,利用这个刷新频率进行页面重绘。
  • 一旦页面不处于浏览器的当前标签,就会自动停止刷新。这就节省了CPU、GPU和电力。


5. requestAnimationFrame 优势

  • CPU节能:使用setTimeout实现的动画,当页面被隐藏或最小化时,setTimeout 仍然在后台执行动画任务,由于此时页面处于不可见或不可用状态,刷新动画是没有意义的,完全是浪费CPU资源。而requestAnimationFrame则完全不同,当页面处理未激活的状态下,该页面的屏幕刷新任务也会被系统暂停,因此跟着系统步伐走的requestAnimationFrame也会停止渲染,当页面被激活时,动画就从上次停留的地方继续执行,有效节省了CPU开销。
  • 函数节流:在高频率事件(resize,scroll等)中,为了防止在一个刷新间隔内发生多次函数执行,使用requestAnimationFrame可保证每个刷新间隔内,函数只被执行一次,这样既能保证流畅性,也能更好的节省函数执行的开销。一个刷新间隔内函数执行多次时没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来。

6. requestAnimationFrame 优雅降级

由于requestAnimationFrame目前还存在兼容性问题,而且不同的浏览器还需要带不同的前缀。因此需要通过优雅降级的方式对requestAnimationFrame进行封装,优先使用高级特性,然后再根据不同浏览器的情况进行回退,直止只能使用setTimeout的情况。

// 开源组件库 ngx-tethys里的requestAnimationFrame优雅降级
const availablePrefixes = ['moz', 'ms', 'webkit'];

function requestAnimationFramePolyfill(): typeof requestAnimationFrame {
    let lastTime = 0;
    return function(callback: FrameRequestCallback): number {
        const currTime = new Date().getTime();
        const timeToCall = Math.max(0, 16 - (currTime - lastTime));
        const id = setTimeout(() => {
            callback(currTime + timeToCall);
        }, timeToCall) as any; // setTimeout type warn
        lastTime = currTime + timeToCall;
        return id;
    };
}

function getRequestAnimationFrame(): typeof requestAnimationFrame {
    if (typeof window === 'undefined') {
        return () => 0;
    }
    if (window.requestAnimationFrame) {
        // https://github.com/vuejs/vue/issues/4465
        return window.requestAnimationFrame.bind(window);
    }

    const prefix = availablePrefixes.filter(key => `${key}RequestAnimationFrame` in window)[0];

    return prefix ? (window as any)[`${prefix}RequestAnimationFrame`] : requestAnimationFramePolyfill();
}

export function cancelRequestAnimationFrame(id: number): any {
    if (typeof window === 'undefined') {
        return null;
    }
    if (window.cancelAnimationFrame) {
        return window.cancelAnimationFrame(id);
    }
    const prefix = availablePrefixes.filter(
        key => `${key}CancelAnimationFrame` in window || `${key}CancelRequestAnimationFrame` in window
    )[0];

    return prefix
        ? ((window as any)[`${prefix}CancelAnimationFrame`] || (window as any)[`${prefix}CancelRequestAnimationFrame`])
              // @ts-ignore
              .call(this, id)
        : clearTimeout(id);
}

export const reqAnimFrame = getRequestAnimationFrame();


三、使用 passive 可改善的滚屏性能

移动端的一些事件比如 touchstart 、 touchmove 、 touchend 、 touchcancel 等,如果在这些事件中阻止默认行为,页面会被禁止滚动或缩放。


而浏览器无法事先知道一个监听器是否会禁止默认行为,要等监听器执行之后,才会去执行默认行为。而监听器的执行是要耗时的,如果在 event.preventDefault() 之前耗时了 2秒 ,这样就会导致页面卡顿。


为了提升此种场景下的滚动体验,我们需要有一个参数来告诉浏览器,我的事件监听器中不会有 event.preventDefault() ,你可以不用等监听器执行完毕,请尽情的滚动吧。所以有了 passive 属性。为了兼容之前的 useCapture ,把最后一个参数改成了一个对象 options。


所以在绑定移动端相关的 touch 和滚动事件时,尽可能使用 { passive: true } 来提升性能和体验,避免出现页面卡顿。


然而,不是所有的浏览器都支持 passive 特性,不支持 passive 特性的浏览器会把最后一个参数当作 useCapture ,所以需要这段精妙的代码判断是否支持 passive 特性:

// Test via a getter in the options object to see 
// if the passive property is accessed
var supportsPassive = false;
try {
  var opts = Object.defineProperty({}, 'passive', {
    get: function() {
      supportsPassive = true;
    }
  });
  window.addEventListener("test", null, opts);
} catch (e) {}


// Use our detect's results. 
// passive applied if supported, capture will be false either way.
elem.addEventListener(
  'touchstart',
  fn,
  supportsPassive ? { passive: true } : false
);

通过 Object.defineProperty 设置一个 passive 的 get 访问器,添加一个 test 的事件,当浏览器支持的时候会调用 get 访问器,在 get 访问器中设置 supportsPassive。


在Angular框架中,Angular CDK 早已在 @angular/cdk/platform 模块提供了normalizePassiveListenerOptions({passive: true}) 供我们解决兼容性的问题,核心代码如下:

/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.io/license
 */

/** Cached result of whether the user's browser supports passive event listeners. */
let supportsPassiveEvents: boolean;

/**
 * Checks whether the user's browser supports passive event listeners.
 * See: https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md
 */
export function supportsPassiveEventListeners(): boolean {
  if (supportsPassiveEvents == null && typeof window !== 'undefined') {
    try {
      window.addEventListener(
        'test',
        null!,
        Object.defineProperty({}, 'passive', {
          get: () => (supportsPassiveEvents = true),
        }),
      );
   } finally {
      supportsPassiveEvents = supportsPassiveEvents || false;
    }
  }

  return supportsPassiveEvents;
}

/**
 * Normalizes an `AddEventListener` object to something that can be passed
 * to `addEventListener` on any browser, no matter whether it supports the
 * `options` parameter.
 * @param options Object to be normalized.
 */
export function normalizePassiveListenerOptions(
  options: AddEventListenerOptions,
): AddEventListenerOptions | boolean {
  return supportsPassiveEventListeners() ? options : !!options.capture;
}

添加绑定事件支持 passive 参数的相关Issue:https://github.com/angular/angular/issues/8866

<think>好的,用户想了解如何处理滚动事件或解决相关问题。我需要根据提供的引用资料来组织回答。首先,滚动事件的基本用法,比如在Vue中添加@scroll监听,并获取滚动位置,如引用[1]中的例子。然后可能需要提到不同框架或环境中的实现差异,比如引用[2]中的onScroll语法,还有引用[3]中的scroll-view组件问题。 接着,常见问题比如事件未触发,引用[3]提到需要检查是否在滚动容器内正确绑定事件,或者是否因为阻止了默认行为,如引用[4]中的touch事件阻止导致滚动禁止。另外,性能优化方面,引用[4]提到节流requestAnimationFrame,这点需要强调,避免频繁触发影响性能。 用户可能需要具体的代码示例,比如Vue中的处理,还有如何解决到达顶部触发下拉刷新,如引用[3]中的currentPosi判断。还要注意跨平台或不同开发环境的注意事项,比如在小程序中使用scroll-view的情况。 最后,整理步骤,分点说明实现方法、常见问题和优化建议,确保结构清晰。同时生成相关问题,覆盖滚动事件的应用场景、优化、调试和兼容性问题。</think>### 滚动事件的实现与处理方法 #### 一、滚动事件基础实现 1. **添加滚动监听** 在Vue等框架中,可通过`@scroll`绑定事件到容器,并使用`ref`获取DOM元素: ```html <div class="window" @scroll="scrollEvent" ref="scrollContainer"></div> ``` 对应方法中获取滚动位置: ```javascript scrollEvent() { console.log(this.$refs.scrollContainer.scrollTop); // 垂直滚动距离 } ``` 此方法适用于监听滚动偏移量[^1][^2]。 2. **跨平台语法差异** - **HarmonyOS ArkUI**:使用`onScroll`回调函数直接获取坐标偏移量: ```javascript .onScroll((x, y) => { console.log(`X偏移: ${x}vp, Y偏移: ${y}vp`); }) ``` 需结合`scroller.currentOffset()`获取实时位置[^2]。 - **小程序(如uni-app)**:在`scroll-view`组件中使用`@scroll`,并通过`e.detail`获取滚动数据: ```javascript scroll(e) { console.log('垂直滚动:', e.detail.scrollTop); } ``` 需注意组件层级和样式限制[^3]。 --- #### 二、常见问题与解决方案 1. **滚动事件未触发** - **容器限制**:确保滚动容器有固定高度且内容溢出(如设置`height: 300px; overflow: auto`)。 - **默认行为冲突**:若在移动端阻止了`touchmove`事件,需避免完全阻止默认行为[^4]。 - **框架兼容性**:在小程序中使用`scroll-view`时,需替换原生`div`标签[^3]。 2. **性能优化** - **节流(Throttle)**:减少事件触发频率,例如每200ms执行一次: ```javascript let lastTime = 0; scrollEvent() { const now = Date.now(); if (now - lastTime > 200) { // 处理逻辑 lastTime = now; } } ``` - **`requestAnimationFrame`**:与浏览器渲染帧率同步,避免卡顿。 3. **特定场景处理** - **下拉刷新**:通过滚动位置判断是否到达顶部: ```javascript scroll(e) { this.currentPosi = e.detail.scrollTop; if (this.currentPosi <= 0) { this.triggerRefresh(); // 触发刷新逻辑 } } ``` 需结合防抖避免重复触发。 --- #### 三、关键注意事项 1. **滚动容器标识**:明确指定可滚动区域,避免多层嵌套导致事件失效。 2. **越界回弹处理**:在移动端需测试边界情况,防止滚动异常。 3. **兼容性测试**:不同平台(Web、小程序、原生应用)的滚动事件实现可能差异较大,需针对性适配。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值