掌握 JavaScript 中高效的窗口滚动事件处理:最佳实践和技巧

如果不正确处理这些滚动事件,可能会导致严重的性能问题:

  • 部分是通过阻止 DOM 渲染过程来实现的。
  • 大量注册事件会增加 CPU 的使用率,从而导致移动设备的电池寿命缩短。
  • 如果你依赖滚动事件进行更繁重的计算,那么不可避免地会导致内存泄漏,并进一步降低你的 Web 应用的性能
  • 本文介绍的做法尤其适用于互动性强的网站。它们将帮助您在中心空间更高效地实现滚动事件。让我们深入了解一下。

    **TL:DR**

    您可以在此处找到本文的代码沙箱

    使用专用的滚动实体

    虽然不能直接提高应用的性能,但将所有滚动事件收集到单个函数或类中是个好主意。它使事件的管理和调试更加容易,代码也更加易读。

    function scrollHandler(fns) {
        window.addEventListener('scroll', () => {
            fns.forEach(fn => fn())
        })
    }
    
    
    scrollHandler([
        () => {
            console.log('hello')
        },
        () => {
            console.log('world')
        }
    ])

    浏览器仍将在每次滚动事件时运行所有功能,因此我们还需要进行一些优化。

    使用队列

    当触发滚动事件的回调函数时,浏览器始终会等待所有函数执行完毕。因此,将所有事件处理程序收集到单个实体中可能会导致巨大的性能下降。幸运的是,我们可以通过使用队列来缓解这种情况。

    请注意,我们使用数组作为队列结构,这可能不是最有效的选择。查看本文,了解如何在 Javascript 中实现自己的队列结构

    function scrollHandler(fns) {
        window.addEventListener('scroll', () => {
            const queue = [...fns]
            function next() {
                const fn = queue.shift()
                if (fn) {
                    fn()
                    requestAnimationFrame(next)
                }
            }
            next()
        })
    }

    通过使用“requestAnimationFrame”,我们让浏览器有时间在调用队列中的下一个函数之前处理其他任务。但是,队列仍然无法解决每次用户滚动时都会触发每个事件的问题。有两种方法可以解决这个问题。

    延迟(节流)滚动事件

    最好不要在每次滚动时都调用每个函数,而是提供每秒固定的速率。这就是节流的目的。

    我们可以在前两种方法的基础上实现一个节流功能,每次监听滚动事件但每 200 毫秒仅执行一次。

    function scrollHandlerThrottle(fns) {
        let scrolling = false;
    
        window.addEventListener('scroll', () => {
            if (!scrolling) {
                scrolling = true;
    
                const queue = [...fns];
                function next() {
                    const fn = queue.shift();
                    if (fn) {
                        fn();
                        requestAnimationFrame(next);
                    }
                }
                next();
    
                setTimeout(() => {
                    scrolling = false;
                }, 200);
            }
        })
    }

    因此,函数调用的最大数量被限制为每秒 5 次。您可以通过更改“setTimeout”的第二个参数来调整限制。

    节流与高效队列结构相结合是缓解浏览器阻塞过程的好方法。但是,如果您想进一步限制函数执行,还有另一种方法。

    等待(防抖动)滚动事件

    节流的另一种方法是防抖动。在这种情况下,这意味着等待用户完成滚动。虽然与节流相比交互性较差,但它是一种很好的替代方案,并且根据用例,也更高效。

    我们要做的就是实现一个在初始滚动事件注册后 200 毫秒调用的超时。

    function scrollHandlerDevounce(fns) {
        let scrollTimeout = null;
    
        window.addEventListener('scroll', () => {
            if (scrollTimeout) {
                clearInterval(scrollTimeout);
            }
    
            scrollTimeout = setTimeout(() => {
                const queue = [...fns];
                function next() {
                    const fn = queue.shift();
                    if (fn) {
                        fn();
                        requestAnimationFrame(next);
                    }
                }
                next();
            }, 200);
        });
    }

    每当用户滚动时,只有当滚动停止相当长一段时间时才会触发事件。

    您甚至可以更进一步,将这些方法与其他浏览器功能(例如,观察者 API)相结合,以吸引用户的兴趣或执行异步操作。