pin_drop当前位置:知识文库 ❯ 图文

JS集成:HTML onscroll 事 - 详细教程与实战指南

教程简介

onscroll 事件在元素内容滚动时触发,是实现滚动驱动交互的核心事件。它广泛应用于无限滚动加载、滚动动画、回到顶部按钮、导航栏固定、视差效果等场景。然而,scroll 事件也是最容易引发性能问题的事件之一,因为它在滚动过程中会高频触发,如果不加以优化,可能导致页面卡顿。

本教程将深入讲解 onscroll 事件的核心概念、性能问题与优化策略(节流与防抖)、滚动位置获取、滚动到指定位置、无限滚动实现以及滚动动画效果。


核心概念

onscroll 事件定义

scroll 事件在以下情况触发:

  • 用户通过鼠标滚轮滚动页面

  • 用户通过拖拽滚动条滚动

  • 用户通过键盘(方向键、Page Up/Down、Space)滚动

  • JavaScript 代码通过 scrollTo()scrollIntoView() 等方法触发滚动

scroll 事件的关键特性

特性 说明
触发频率 极高,滚动过程中持续触发
冒泡行为 不冒泡,但在 document 上可监听
默认行为 不可取消(无法阻止滚动本身)
触发时机 滚动发生后触发(非滚动前)
性能影响 处理不当会导致严重性能问题

滚动位置属性

属性 说明 适用对象
scrollTop 垂直滚动距离 Element
scrollLeft 水平滚动距离 Element
scrollY 垂直滚动距离 Window
scrollX 水平滚动距离 Window
pageYOffset scrollY 的别名 Window
scrollHeight 元素内容总高度 Element
scrollWidth 元素内容总宽度 Element

节流(Throttle)与防抖(Debounce)

策略 原理 适用场景 效果
节流 固定时间间隔执行一次 滚动位置检测、动画更新 保证最低执行频率
防抖 停止滚动后执行一次 滚动结束后的处理 减少执行次数
requestAnimationFrame 每帧执行一次 滚动动画、视觉更新 与屏幕刷新同步

语法与用法

HTML 属性方式

代码示例

<element onscroll="handleScroll()">内容</element>

DOM 属性方式

代码示例

element.onscroll = function() {
    // 处理逻辑
};

addEventListener 方式(推荐)

代码示例

// 监听元素滚动
element.addEventListener('scroll', function() {
    // 处理逻辑
});

// 监听页面滚动
window.addEventListener('scroll', function() {
    // 处理逻辑
});

获取滚动位置

代码示例

// 页面滚动位置
const scrollY = window.scrollY || window.pageYOffset;
const scrollX = window.scrollX || window.pageXOffset;

// 元素滚动位置
const scrollTop = element.scrollTop;
const scrollLeft = element.scrollLeft;

滚动到指定位置

代码示例

// window 滚动
window.scrollTo(0, 500);                  // 滚动到 (0, 500)
window.scrollTo({ top: 500, behavior: 'smooth' }); // 平滑滚动

// 元素滚动
element.scrollTop = 500;                  // 直接设置
element.scrollTo({ top: 500, behavior: 'smooth' }); // 平滑滚动

// 滚动到元素可见
element.scrollIntoView({ behavior: 'smooth', block: 'start' });

代码示例

示例1:基础滚动事件与位置检测

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>基础滚动事件与位置检测</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            margin: 0;
            padding: 0;
        }
        .content {
            padding: 40px;
            max-width: 800px;
            margin: 0 auto;
        }
        .section {
            height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 32px;
            font-weight: bold;
            border-bottom: 1px solid #eee;
        }
        .section:nth-child(odd) { background: #e3f2fd; }
        .section:nth-child(even) { background: #f3e5f5; }
        .scroll-indicator {
            position: fixed;
            top: 0;
            left: 0;
            height: 4px;
            background: linear-gradient(90deg, #2196F3, #E91E63);
            transition: width 0.1s;
            z-index: 1000;
        }
        .scroll-info {
            position: fixed;
            bottom: 20px;
            right: 20px;
            background: rgba(0,0,0,0.8);
            color: white;
            padding: 15px 20px;
            border-radius: 10px;
            font-size: 14px;
            z-index: 1000;
            min-width: 200px;
        }
        .scroll-info div {
            margin: 4px 0;
        }
        .scroll-info .label {
            color: #aaa;
            font-size: 12px;
        }
        .scroll-info .value {
            color: #4fc3f7;
            font-weight: bold;
        }
    </style>
</head>
<body>
    <div class="scroll-indicator" id="scrollIndicator"></div>

    <div class="content">
        <div class="section">第一屏 - 向下滚动</div>
        <div class="section">第二屏</div>
        <div class="section">第三屏</div>
        <div class="section">第四屏</div>
        <div class="section">第五屏 - 到底了</div>
    </div>

    <div class="scroll-info" id="scrollInfo">
        <div><span class="label">滚动位置:</span><span class="value" id="scrollPos">0</span>px</div>
        <div><span class="label">页面高度:</span><span class="value" id="pageHeight">0</span>px</div>
        <div><span class="label">滚动百分比:</span><span class="value" id="scrollPercent">0</span>%</div>
        <div><span class="label">当前区域:</span><span class="value" id="currentSection">-</span></div>
    </div>

    <script>
        const scrollIndicator = document.getElementById('scrollIndicator');
        const scrollPosEl = document.getElementById('scrollPos');
        const pageHeightEl = document.getElementById('pageHeight');
        const scrollPercentEl = document.getElementById('scrollPercent');
        const currentSectionEl = document.getElementById('currentSection');
        const sections = document.querySelectorAll('.section');

        // 使用节流优化
        let ticking = false;

        window.addEventListener('scroll', function() {
            if (!ticking) {
                requestAnimationFrame(updateScrollInfo);
                ticking = true;
            }
        });

        function updateScrollInfo() {
            const scrollY = window.scrollY || window.pageYOffset;
            const docHeight = document.documentElement.scrollHeight - window.innerHeight;
            const scrollPercent = docHeight > 0 ? Math.round((scrollY / docHeight) * 100) : 0;

            // 更新进度条
            scrollIndicator.style.width = scrollPercent + '%';

            // 更新信息面板
            scrollPosEl.textContent = Math.round(scrollY);
            pageHeightEl.textContent = document.documentElement.scrollHeight;
            scrollPercentEl.textContent = scrollPercent;

            // 检测当前区域
            sections.forEach((section, index) => {
                const rect = section.getBoundingClientRect();
                if (rect.top <= window.innerHeight / 2 && rect.bottom >= window.innerHeight / 2) {
                    currentSectionEl.textContent = '第' + (index + 1) + '屏';
                }
            });

            ticking = false;
        }

        // 初始化
        updateScrollInfo();
    </script>
</body>
</html>

示例2:节流与防抖对比

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>节流与防抖对比</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            margin: 0;
            padding: 20px;
            background: #f5f5f5;
        }
        .scroll-container {
            height: 300px;
            overflow-y: auto;
            border: 2px solid #ddd;
            border-radius: 8px;
            background: white;
            margin-bottom: 20px;
        }
        .scroll-content {
            height: 2000px;
            padding: 20px;
            background: linear-gradient(to bottom, #e3f2fd, #f3e5f5, #fff3e0, #e8f5e9);
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 20px;
            margin-bottom: 20px;
        }
        .stat-card {
            padding: 20px;
            background: white;
            border-radius: 12px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
            text-align: center;
        }
        .stat-card h3 {
            margin-top: 0;
            font-size: 16px;
        }
        .stat-value {
            font-size: 36px;
            font-weight: bold;
            margin: 10px 0;
        }
        .stat-card:nth-child(1) .stat-value { color: #f44336; }
        .stat-card:nth-child(2) .stat-value { color: #4CAF50; }
        .stat-card:nth-child(3) .stat-value { color: #2196F3; }
        .stat-desc {
            font-size: 13px;
            color: #666;
        }
        .log-container {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 20px;
        }
        .log-panel {
            padding: 15px;
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
        }
        .log-panel h4 {
            margin-top: 0;
        }
        .log-panel .log {
            height: 150px;
            overflow-y: auto;
            font-size: 12px;
            font-family: monospace;
            background: #f5f5f5;
            padding: 8px;
            border-radius: 4px;
        }
    </style>
</head>
<body>
    <h1>节流与防抖对比</h1>
    <p>在下方区域中滚动,观察三种处理方式的触发次数差异。</p>

    <div class="scroll-container" id="scrollContainer">
        <div class="scroll-content">
            <h2>滚动此区域</h2>
            <p>快速滚动并观察右侧的触发次数对比。</p>
        </div>
    </div>

    <div class="stats-grid">
        <div class="stat-card">
            <h3>无优化</h3>
            <div class="stat-value" id="rawCount">0</div>
            <div class="stat-desc">每次滚动都触发</div>
        </div>
        <div class="stat-card">
            <h3>节流 (100ms)</h3>
            <div class="stat-value" id="throttleCount">0</div>
            <div class="stat-desc">每100ms最多触发一次</div>
        </div>
        <div class="stat-card">
            <h3>防抖 (200ms)</h3>
            <div class="stat-value" id="debounceCount">0</div>
            <div class="stat-desc">停止滚动200ms后触发</div>
        </div>
    </div>

    <div class="log-container">
        <div class="log-panel">
            <h4>无优化日志</h4>
            <div class="log" id="rawLog"></div>
        </div>
        <div class="log-panel">
            <h4>节流日志</h4>
            <div class="log" id="throttleLog"></div>
        </div>
        <div class="log-panel">
            <h4>防抖日志</h4>
            <div class="log" id="debounceLog"></div>
        </div>
    </div>

    <script>
        const container = document.getElementById('scrollContainer');
        let rawCount = 0, throttleCount = 0, debounceCount = 0;

        // 无优化
        container.addEventListener('scroll', function() {
            rawCount++;
            document.getElementById('rawCount').textContent = rawCount;
            addLog('rawLog', rawCount);
        });

        // 节流
        function throttle(fn, delay) {
            let lastTime = 0;
            return function(...args) {
                const now = Date.now();
                if (now - lastTime >= delay) {
                    lastTime = now;
                    fn.apply(this, args);
                }
            };
        }

        container.addEventListener('scroll', throttle(function() {
            throttleCount++;
            document.getElementById('throttleCount').textContent = throttleCount;
            addLog('throttleLog', throttleCount);
        }, 100));

        // 防抖
        function debounce(fn, delay) {
            let timer = null;
            return function(...args) {
                clearTimeout(timer);
                timer = setTimeout(() => fn.apply(this, args), delay);
            };
        }

        container.addEventListener('scroll', debounce(function() {
            debounceCount++;
            document.getElementById('debounceCount').textContent = debounceCount;
            addLog('debounceLog', debounceCount);
        }, 200));

        function addLog(id, count) {
            const log = document.getElementById(id);
            const time = new Date().toLocaleTimeString('zh-CN', { hour12: false });
            log.innerHTML += `[${time}] 第${count}次触发<br>`;
            log.scrollTop = log.scrollHeight;
        }
    </script>
</body>
</html>

示例3:滚动位置获取与回到顶部

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>滚动位置获取与回到顶部</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            margin: 0;
            padding: 0;
        }
        .header {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            padding: 15px 30px;
            background: white;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            z-index: 100;
            display: flex;
            justify-content: space-between;
            align-items: center;
            transition: transform 0.3s, box-shadow 0.3s;
        }
        .header.hidden {
            transform: translateY(-100%);
        }
        .header.scrolled {
            box-shadow: 0 4px 20px rgba(0,0,0,0.15);
        }
        .header h1 {
            margin: 0;
            font-size: 20px;
        }
        .scroll-badge {
            padding: 4px 12px;
            background: #e3f2fd;
            border-radius: 12px;
            font-size: 13px;
            color: #1565C0;
        }
        .content {
            margin-top: 70px;
            padding: 40px;
            max-width: 800px;
            margin-left: auto;
            margin-right: auto;
        }
        .content p {
            line-height: 1.8;
            margin-bottom: 20px;
            color: #333;
        }
        .back-to-top {
            position: fixed;
            bottom: 30px;
            right: 30px;
            width: 50px;
            height: 50px;
            background: #2196F3;
            color: white;
            border: none;
            border-radius: 50%;
            font-size: 24px;
            cursor: pointer;
            box-shadow: 0 4px 15px rgba(33, 150, 243, 0.4);
            opacity: 0;
            visibility: hidden;
            transform: translateY(20px);
            transition: all 0.3s ease;
            z-index: 100;
        }
        .back-to-top.visible {
            opacity: 1;
            visibility: visible;
            transform: translateY(0);
        }
        .back-to-top:hover {
            background: #1565C0;
            transform: translateY(-3px);
        }
        .section-marker {
            padding: 60px 0;
            border-bottom: 1px solid #eee;
        }
        .section-marker h2 {
            color: #2196F3;
        }
        .nav-dots {
            position: fixed;
            right: 20px;
            top: 50%;
            transform: translateY(-50%);
            z-index: 100;
        }
        .nav-dot {
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background: #ccc;
            margin: 10px 0;
            cursor: pointer;
            transition: all 0.3s;
        }
        .nav-dot.active {
            background: #2196F3;
            transform: scale(1.3);
        }
    </style>
</head>
<body>
    <div class="header" id="header">
        <h1>滚动位置检测</h1>
        <span class="scroll-badge" id="scrollBadge">0px</span>
    </div>

    <div class="content">
        <div class="section-marker" id="section1">
            <h2>第一节:简介</h2>
            <p>滚动页面可以看到导航栏的智能隐藏效果。向下滚动时导航栏隐藏,向上滚动时导航栏显示。这是通过检测滚动方向实现的。</p>
            <p>右侧的导航点会根据当前滚动位置高亮对应的区域。点击导航点可以平滑滚动到对应区域。</p>
        </div>
        <div class="section-marker" id="section2">
            <h2>第二节:滚动位置</h2>
            <p>通过 window.scrollY 可以获取当前页面的垂直滚动位置。结合 document.documentElement.scrollHeight 和 window.innerHeight,可以计算出滚动百分比。</p>
            <p>这些信息对于实现进度条、回到顶部按钮、无限滚动等功能至关重要。</p>
        </div>
        <div class="section-marker" id="section3">
            <h2>第三节:性能优化</h2>
            <p>scroll 事件在滚动过程中会高频触发,直接在事件处理程序中执行 DOM 操作会导致性能问题。使用 requestAnimationFrame 可以确保更新操作与屏幕刷新同步。</p>
            <p>另一种优化方式是使用 Passive Event Listener,告诉浏览器不会调用 preventDefault(),允许浏览器优化滚动性能。</p>
        </div>
        <div class="section-marker" id="section4">
            <h2>第四节:平滑滚动</h2>
            <p>使用 window.scrollTo({ behavior: 'smooth' }) 可以实现平滑滚动效果。也可以使用 element.scrollIntoView({ behavior: 'smooth' }) 滚动到特定元素。</p>
            <p>对于不支持 smooth 行为的浏览器,可以使用 JavaScript 实现自定义的平滑滚动动画。</p>
        </div>
        <div class="section-marker" id="section5">
            <h2>第五节:总结</h2>
            <p>滚动事件是前端开发中最常用但也最容易出问题的事件之一。正确使用节流、防抖和 requestAnimationFrame 可以有效优化性能。</p>
            <p>结合 IntersectionObserver API 可以更高效地检测元素可见性,减少对 scroll 事件的依赖。</p>
        </div>
    </div>

    <div class="nav-dots" id="navDots">
        <div class="nav-dot" data-target="section1" title="第一节"></div>
        <div class="nav-dot" data-target="section2" title="第二节"></div>
        <div class="nav-dot" data-target="section3" title="第三节"></div>
        <div class="nav-dot" data-target="section4" title="第四节"></div>
        <div class="nav-dot" data-target="section5" title="第五节"></div>
    </div>

    <button class="back-to-top" id="backToTop" title="回到顶部">↑</button>

    <script>
        const header = document.getElementById('header');
        const scrollBadge = document.getElementById('scrollBadge');
        const backToTop = document.getElementById('backToTop');
        const navDots = document.querySelectorAll('.nav-dot');
        const sections = ['section1', 'section2', 'section3', 'section4', 'section5'];

        let lastScrollY = 0;
        let ticking = false;

        // 使用 passive 优化滚动性能
        window.addEventListener('scroll', function() {
            if (!ticking) {
                requestAnimationFrame(updateUI);
                ticking = true;
            }
        }, { passive: true });

        function updateUI() {
            const scrollY = window.scrollY;

            // 更新滚动位置显示
            scrollBadge.textContent = Math.round(scrollY) + 'px';

            // 导航栏智能隐藏
            if (scrollY > lastScrollY && scrollY > 100) {
                header.classList.add('hidden');
            } else {
                header.classList.remove('hidden');
            }
            header.classList.toggle('scrolled', scrollY > 10);

            // 回到顶部按钮
            backToTop.classList.toggle('visible', scrollY > 300);

            // 更新导航点
            updateNavDots(scrollY);

            lastScrollY = scrollY;
            ticking = false;
        }

        function updateNavDots(scrollY) {
            const viewportCenter = scrollY + window.innerHeight / 2;
            let activeIndex = 0;

            sections.forEach((id, index) => {
                const el = document.getElementById(id);
                if (el.offsetTop <= viewportCenter) {
                    activeIndex = index;
                }
            });

            navDots.forEach((dot, index) => {
                dot.classList.toggle('active', index === activeIndex);
            });
        }

        // 回到顶部
        backToTop.addEventListener('click', function() {
            window.scrollTo({ top: 0, behavior: 'smooth' });
        });

        // 导航点点击
        navDots.forEach(dot => {
            dot.addEventListener('click', function() {
                const target = document.getElementById(this.dataset.target);
                target.scrollIntoView({ behavior: 'smooth', block: 'start' });
            });
        });
    </script>
</body>
</html>

示例4:无限滚动实现

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>无限滚动实现</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            margin: 0;
            padding: 20px;
            background: #f5f5f5;
        }
        .post-list {
            max-width: 600px;
            margin: 0 auto;
        }
        .post {
            background: white;
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 16px;
            box-shadow: 0 2px 8px rgba(0,0,0,0.08);
            opacity: 0;
            transform: translateY(20px);
            animation: fadeIn 0.4s ease forwards;
        }
        @keyframes fadeIn {
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
        .post-header {
            display: flex;
            align-items: center;
            gap: 12px;
            margin-bottom: 12px;
        }
        .avatar {
            width: 40px;
            height: 40px;
            border-radius: 50%;
            background: linear-gradient(135deg, #667eea, #764ba2);
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-weight: bold;
        }
        .post-author {
            font-weight: bold;
            font-size: 15px;
        }
        .post-time {
            font-size: 12px;
            color: #999;
        }
        .post-content {
            font-size: 14px;
            line-height: 1.6;
            color: #333;
        }
        .loading {
            text-align: center;
            padding: 30px;
            color: #999;
        }
        .loading-spinner {
            display: inline-block;
            width: 30px;
            height: 30px;
            border: 3px solid #e0e0e0;
            border-top-color: #2196F3;
            border-radius: 50%;
            animation: spin 0.8s linear infinite;
        }
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        .sentinel {
            height: 1px;
        }
    </style>
</head>
<body>
    <h1 style="text-align: center;">无限滚动</h1>

    <div class="post-list" id="postList"></div>

    <div class="loading" id="loading" style="display: none;">
        <div class="loading-spinner"></div>
        <p>加载中...</p>
    </div>

    <div class="sentinel" id="sentinel"></div>

    <script>
        const postList = document.getElementById('postList');
        const loading = document.getElementById('loading');
        const sentinel = document.getElementById('sentinel');
        let page = 0;
        let isLoading = false;
        let hasMore = true;

        // 模拟数据
        const authors = ['张三', '李四', '王五', '赵六', '孙七', '周八', '吴九', '郑十'];
        const contents = [
            '今天学习了 JavaScript 的事件循环机制,对宏任务和微任务有了更深的理解。',
            '分享一个 CSS 技巧:使用 backdrop-filter 可以轻松实现毛玻璃效果。',
            'Vue 3 的 Composition API 真的太好用了,代码组织更加灵活。',
            '推荐一个 VS Code 插件:Error Lens,可以直接在代码行显示错误信息。',
            'TypeScript 的类型体操虽然复杂,但掌握后能极大提高代码质量。',
            '今天用 CSS Grid 实现了一个复杂的响应式布局,比 Flexbox 方便多了。',
            'React Server Components 是一个革命性的概念,值得深入学习。',
            '分享一个性能优化技巧:使用 IntersectionObserver 替代 scroll 事件监听。'
        ];

        // 方式一:使用 IntersectionObserver(推荐)
        const observer = new IntersectionObserver((entries) => {
            if (entries[0].isIntersecting && !isLoading && hasMore) {
                loadMore();
            }
        }, {
            rootMargin: '200px' // 提前200px开始加载
        });

        observer.observe(sentinel);

        // 方式二:使用 scroll 事件(备选)
        /*
        let scrollTicking = false;
        window.addEventListener('scroll', function() {
            if (scrollTicking) return;
            scrollTicking = true;

            requestAnimationFrame(() => {
                const scrollBottom = window.scrollY + window.innerHeight;
                const docHeight = document.documentElement.scrollHeight;

                if (scrollBottom >= docHeight - 200 && !isLoading && hasMore) {
                    loadMore();
                }
                scrollTicking = false;
            });
        }, { passive: true });
        */

        function loadMore() {
            if (isLoading || !hasMore) return;
            isLoading = true;
            loading.style.display = 'block';

            // 模拟网络请求
            setTimeout(() => {
                const fragment = document.createDocumentFragment();

                for (let i = 0; i < 5; i++) {
                    const post = createPost(page * 5 + i);
                    fragment.appendChild(post);
                }

                postList.appendChild(fragment);
                page++;

                // 模拟数据结束
                if (page >= 10) {
                    hasMore = false;
                    loading.innerHTML = '<p>没有更多内容了</p>';
                }

                isLoading = false;
                loading.style.display = hasMore ? 'none' : 'block';
            }, 800);
        }

        function createPost(index) {
            const post = document.createElement('div');
            post.className = 'post';
            post.style.animationDelay = (index % 5) * 0.1 + 's';

            const author = authors[index % authors.length];
            const content = contents[index % contents.length];
            const hours = Math.floor(Math.random() * 24);

            post.innerHTML = `
                <div class="post-header">
                    <div class="avatar">${author[0]}</div>
                    <div>
                        <div class="post-author">${author}</div>
                        <div class="post-time">${hours}小时前</div>
                    </div>
                </div>
                <div class="post-content">${content}</div>
            `;

            return post;
        }

        // 初始加载
        loadMore();
    </script>
</body>
</html>

示例5:滚动动画效果

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>滚动动画效果</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            margin: 0;
            padding: 0;
            background: #0a0a1a;
            color: white;
        }
        .hero {
            height: 100vh;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            text-align: center;
            position: relative;
            overflow: hidden;
        }
        .hero h1 {
            font-size: 48px;
            margin-bottom: 20px;
            opacity: 0;
            transform: translateY(30px);
            transition: all 0.8s ease;
        }
        .hero h1.visible {
            opacity: 1;
            transform: translateY(0);
        }
        .hero p {
            font-size: 20px;
            color: #aaa;
            opacity: 0;
            transform: translateY(20px);
            transition: all 0.8s ease 0.2s;
        }
        .hero p.visible {
            opacity: 1;
            transform: translateY(0);
        }
        .scroll-hint {
            position: absolute;
            bottom: 40px;
            animation: bounce 2s infinite;
        }
        @keyframes bounce {
            0%, 100% { transform: translateY(0); }
            50% { transform: translateY(10px); }
        }
        .feature-section {
            padding: 100px 40px;
            max-width: 1000px;
            margin: 0 auto;
        }
        .feature-section h2 {
            text-align: center;
            font-size: 36px;
            margin-bottom: 60px;
        }
        .feature-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 30px;
        }
        .feature-card {
            background: #1a1a2e;
            border-radius: 16px;
            padding: 30px;
            text-align: center;
            opacity: 0;
            transform: translateY(40px);
            transition: all 0.6s ease;
        }
        .feature-card.visible {
            opacity: 1;
            transform: translateY(0);
        }
        .feature-card:nth-child(2) { transition-delay: 0.15s; }
        .feature-card:nth-child(3) { transition-delay: 0.3s; }
        .feature-icon {
            font-size: 48px;
            margin-bottom: 20px;
        }
        .feature-card h3 {
            margin: 0 0 10px 0;
            font-size: 18px;
        }
        .feature-card p {
            font-size: 14px;
            color: #888;
            line-height: 1.6;
        }
        .parallax-section {
            height: 60vh;
            display: flex;
            align-items: center;
            justify-content: center;
            position: relative;
            overflow: hidden;
        }
        .parallax-bg {
            position: absolute;
            inset: -20%;
            background: linear-gradient(135deg, #667eea, #764ba2, #f093fb);
            opacity: 0.3;
            will-change: transform;
        }
        .parallax-text {
            font-size: 42px;
            font-weight: bold;
            text-align: center;
            z-index: 1;
            opacity: 0;
            transition: opacity 0.8s;
        }
        .parallax-text.visible {
            opacity: 1;
        }
        .counter-section {
            padding: 80px 40px;
            background: #111128;
        }
        .counter-grid {
            display: grid;
            grid-template-columns: repeat(4, 1fr);
            gap: 30px;
            max-width: 900px;
            margin: 0 auto;
            text-align: center;
        }
        .counter-item {
            opacity: 0;
            transform: scale(0.8);
            transition: all 0.5s ease;
        }
        .counter-item.visible {
            opacity: 1;
            transform: scale(1);
        }
        .counter-value {
            font-size: 48px;
            font-weight: bold;
            color: #667eea;
        }
        .counter-label {
            font-size: 14px;
            color: #888;
            margin-top: 8px;
        }
    </style>
</head>
<body>
    <div class="hero">
        <h1 id="heroTitle">滚动动画效果</h1>
        <p id="heroDesc">向下滚动,体验各种滚动驱动的动画效果</p>
        <div class="scroll-hint">▼ 向下滚动</div>
    </div>

    <div class="feature-section">
        <h2>核心功能</h2>
        <div class="feature-grid">
            <div class="feature-card" data-animate>
                <div class="feature-icon">⚡</div>
                <h3>高性能</h3>
                <p>使用 IntersectionObserver 实现高效的滚动检测,避免 scroll 事件的性能问题</p>
            </div>
            <div class="feature-card" data-animate>
                <div class="feature-icon">🎨</div>
                <h3>丰富动画</h3>
                <p>支持淡入、滑入、缩放等多种动画效果,可自定义延迟和持续时间</p>
            </div>
            <div class="feature-card" data-animate>
                <div class="feature-icon">🚀</div>
                <h3>平滑体验</h3>
                <p>基于 CSS transition 的动画,利用 GPU 加速,确保 60fps 流畅体验</p>
            </div>
        </div>
    </div>

    <div class="parallax-section">
        <div class="parallax-bg" id="parallaxBg"></div>
        <div class="parallax-text" data-animate>视差滚动效果</div>
    </div>

    <div class="counter-section">
        <div class="counter-grid">
            <div class="counter-item" data-animate data-count="1200">
                <div class="counter-value" data-target="1200">0</div>
                <div class="counter-label">项目数量</div>
            </div>
            <div class="counter-item" data-animate data-count="850">
                <div class="counter-value" data-target="850">0</div>
                <div class="counter-label">满意客户</div>
            </div>
            <div class="counter-item" data-animate data-count="50">
                <div class="counter-value" data-target="50">0</div>
                <div class="counter-label">团队成员</div>
            </div>
            <div class="counter-item" data-animate data-count="99">
                <div class="counter-value" data-target="99">0</div>
                <div class="counter-label">好评率%</div>
            </div>
        </div>
    </div>

    <script>
        // 使用 IntersectionObserver 实现滚动动画
        const animateElements = document.querySelectorAll('[data-animate]');

        const observer = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    entry.target.classList.add('visible');

                    // 如果是计数器,启动计数动画
                    const counter = entry.target.querySelector('.counter-value');
                    if (counter) {
                        animateCounter(counter);
                    }

                    // 只触发一次
                    observer.unobserve(entry.target);
                }
            });
        }, {
            threshold: 0.2,
            rootMargin: '0px 0px -50px 0px'
        });

        animateElements.forEach(el => observer.observe(el));

        // Hero 区域动画
        setTimeout(() => {
            document.getElementById('heroTitle').classList.add('visible');
            document.getElementById('heroDesc').classList.add('visible');
        }, 300);

        // 视差滚动效果
        const parallaxBg = document.getElementById('parallaxBg');
        let parallaxTicking = false;

        window.addEventListener('scroll', function() {
            if (!parallaxTicking) {
                requestAnimationFrame(() => {
                    const scrollY = window.scrollY;
                    const section = parallaxBg.parentElement;
                    const rect = section.getBoundingClientRect();

                    if (rect.top < window.innerHeight && rect.bottom > 0) {
                        const progress = (window.innerHeight - rect.top) / (window.innerHeight + rect.height);
                        parallaxBg.style.transform = `translateY(${(progress - 0.5) * 60}px)`;
                    }

                    parallaxTicking = false;
                });
                parallaxTicking = true;
            }
        }, { passive: true });

        // 计数器动画
        function animateCounter(el) {
            const target = parseInt(el.dataset.target);
            const duration = 2000;
            const start = performance.now();

            function update(now) {
                const elapsed = now - start;
                const progress = Math.min(elapsed / duration, 1);
                // 使用 easeOutCubic 缓动
                const eased = 1 - Math.pow(1 - progress, 3);
                el.textContent = Math.round(target * eased);

                if (progress < 1) {
                    requestAnimationFrame(update);
                }
            }

            requestAnimationFrame(update);
        }
    </script>
</body>
</html>

示例6:scrollTo 与 scrollIntoView 导航

代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>scrollTo 与 scrollIntoView 导航</title>
    <style>
        body {
            font-family: "Microsoft YaHei", sans-serif;
            margin: 0;
            padding: 0;
        }
        .sidebar {
            position: fixed;
            left: 0;
            top: 0;
            bottom: 0;
            width: 220px;
            background: #1a1a2e;
            color: white;
            padding: 20px 0;
            overflow-y: auto;
        }
        .sidebar h3 {
            padding: 0 20px;
            margin-top: 0;
            font-size: 16px;
            color: #667eea;
        }
        .nav-link {
            display: block;
            padding: 12px 20px;
            color: #ccc;
            text-decoration: none;
            font-size: 14px;
            transition: all 0.2s;
            border-left: 3px solid transparent;
        }
        .nav-link:hover {
            background: rgba(102, 126, 234, 0.1);
            color: white;
        }
        .nav-link.active {
            background: rgba(102, 126, 234, 0.15);
            color: #667eea;
            border-left-color: #667eea;
        }
        .main-content {
            margin-left: 220px;
            padding: 40px;
        }
        .chapter {
            padding: 40px 0;
            border-bottom: 1px solid #eee;
            min-height: 80vh;
        }
        .chapter h2 {
            color: #333;
            margin-bottom: 20px;
        }
        .chapter p {
            line-height: 1.8;
            color: #555;
            max-width: 700px;
        }
        .method-card {
            background: #f5f5f5;
            border-radius: 8px;
            padding: 20px;
            margin: 20px 0;
            border-left: 4px solid #667eea;
        }
        .method-card h4 {
            margin-top: 0;
            color: #667eea;
        }
        code {
            background: #e8eaf6;
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 13px;
        }
    </style>
</head>
<body>
    <div class="sidebar">
        <h3>API 导航</h3>
        <a class="nav-link active" href="#chapter1" data-target="chapter1">scrollTo</a>
        <a class="nav-link" href="#chapter2" data-target="chapter2">scrollIntoView</a>
        <a class="nav-link" href="#chapter3" data-target="chapter3">scrollBy</a>
        <a class="nav-link" href="#chapter4" data-target="chapter4">scrollTo 选项</a>
        <a class="nav-link" href="#chapter5" data-target="chapter5">自定义平滑滚动</a>
    </div>

    <div class="main-content">
        <div class="chapter" id="chapter1">
            <h2>1. window.scrollTo()</h2>
            <p>scrollTo 方法将页面滚动到指定位置。可以接受两个参数(x, y)或一个选项对象。</p>
            <div class="method-card">
                <h4>语法</h4>
                <p><code>window.scrollTo(x, y)</code> - 滚动到绝对位置</p>
                <p><code>window.scrollTo({ top, left, behavior })</code> - 使用选项对象</p>
            </div>
            <div class="method-card">
                <h4>示例</h4>
                <p><code>window.scrollTo(0, 500)</code> - 滚动到顶部 500px</p>
                <p><code>window.scrollTo({ top: 500, behavior: 'smooth' })</code> - 平滑滚动</p>
            </div>
        </div>

        <div class="chapter" id="chapter2">
            <h2>2. element.scrollIntoView()</h2>
            <p>scrollIntoView 方法将元素滚动到视口中可见的位置。</p>
            <div class="method-card">
                <h4>语法</h4>
                <p><code>element.scrollIntoView()</code> - 默认滚动到顶部</p>
                <p><code>element.scrollIntoView({ behavior, block, inline })</code></p>
            </div>
            <div class="method-card">
                <h4>选项</h4>
                <p><code>behavior</code>: 'auto' | 'smooth'</p>
                <p><code>block</code>: 'start' | 'center' | 'end' | 'nearest'</p>
                <p><code>inline</code>: 'start' | 'center' | 'end' | 'nearest'</p>
            </div>
        </div>

        <div class="chapter" id="chapter3">
            <h2>3. window.scrollBy()</h2>
            <p>scrollBy 方法相对于当前滚动位置进行滚动。</p>
            <div class="method-card">
                <h4>语法</h4>
                <p><code>window.scrollBy(x, y)</code> - 相对滚动</p>
                <p><code>window.scrollBy({ top, left, behavior })</code> - 使用选项对象</p>
            </div>
            <div class="method-card">
                <h4>示例</h4>
                <p><code>window.scrollBy(0, 200)</code> - 向下滚动 200px</p>
                <p><code>window.scrollBy({ top: -200, behavior: 'smooth' })</code> - 平滑向上滚动</p>
            </div>
        </div>

        <div class="chapter" id="chapter4">
            <h2>4. scrollTo 选项详解</h2>
            <p>现代浏览器支持 scrollTo 的选项对象形式,提供更灵活的滚动控制。</p>
            <div class="method-card">
                <h4>选项属性</h4>
                <p><code>top</code>: 垂直滚动目标位置(px)</p>
                <p><code>left</code>: 水平滚动目标位置(px)</p>
                <p><code>behavior</code>: 滚动行为,'auto' 为立即跳转,'smooth' 为平滑滚动</p>
            </div>
        </div>

        <div class="chapter" id="chapter5">
            <h2>5. 自定义平滑滚动</h2>
            <p>对于不支持 smooth 行为的浏览器,可以使用 requestAnimationFrame 实现自定义平滑滚动。</p>
            <div class="method-card">
                <h4>实现思路</h4>
                <p>1. 计算起始位置和目标位置的差值</p>
                <p>2. 使用缓动函数计算每帧的位移</p>
                <p>3. 通过 requestAnimationFrame 逐帧更新滚动位置</p>
            </div>
        </div>
    </div>

    <script>
        const navLinks = document.querySelectorAll('.nav-link');
        const chapters = document.querySelectorAll('.chapter');

        // 导航点击 - 使用 scrollIntoView
        navLinks.forEach(link => {
            link.addEventListener('click', function(e) {
                e.preventDefault();
                const targetId = this.dataset.target;
                const targetEl = document.getElementById(targetId);

                targetEl.scrollIntoView({
                    behavior: 'smooth',
                    block: 'start'
                });
            });
        });

        // 滚动时更新导航高亮
        let scrollTicking = false;

        window.addEventListener('scroll', function() {
            if (scrollTicking) return;
            scrollTicking = true;

            requestAnimationFrame(() => {
                const scrollY = window.scrollY + 100;

                chapters.forEach((chapter, index) => {
                    const top = chapter.offsetTop;
                    const bottom = top + chapter.offsetHeight;

                    if (scrollY >= top && scrollY < bottom) {
                        navLinks.forEach(l => l.classList.remove('active'));
                        navLinks[index].classList.add('active');
                    }
                });

                scrollTicking = false;
            });
        }, { passive: true });

        // 自定义平滑滚动函数
        function smoothScrollTo(targetY, duration = 500) {
            const startY = window.scrollY;
            const diff = targetY - startY;
            const startTime = performance.now();

            function step(currentTime) {
                const elapsed = currentTime - startTime;
                const progress = Math.min(elapsed / duration, 1);
                // easeInOutCubic
                const eased = progress < 0.5
                    ? 4 * progress * progress * progress
                    : 1 - Math.pow(-2 * progress + 2, 3) / 2;

                window.scrollTo(0, startY + diff * eased);

                if (progress < 1) {
                    requestAnimationFrame(step);
                }
            }

            requestAnimationFrame(step);
        }
    </script>
</body>
</html>

浏览器兼容性

API Chrome Firefox Safari Edge IE
scroll 事件 全部 全部 全部 全部 6+
window.scrollY 全部 全部 全部 全部 9+
scrollTo 选项对象 45+ 46+ 15.4+ 79+ 不支持
scrollIntoView 选项 61+ 36+ 15.4+ 79+ 不支持
smooth 滚动行为 45+ 46+ 15.4+ 79+ 不支持
Passive Event Listener 51+ 49+ 10+ 15+ 不支持
IntersectionObserver 51+ 55+ 12.1+ 15+ 不支持
CSS scroll-behavior 61+ 36+ 15.4+ 79+ 不支持

注意事项与最佳实践

1. 使用 Passive Event Listener

对于 scroll 事件,始终使用 passive: true 选项,告诉浏览器不会调用 preventDefault(),允许浏览器优化滚动性能。

代码示例

window.addEventListener('scroll', handler, { passive: true });

2. 使用 requestAnimationFrame 节流

不要直接在 scroll 回调中执行 DOM 操作,使用 requestAnimationFrame 确保每帧最多执行一次。

代码示例

let ticking = false;
window.addEventListener('scroll', function() {
    if (!ticking) {
        requestAnimationFrame(updateUI);
        ticking = true;
    }
}, { passive: true });

function updateUI() {
    // 更新 DOM
    ticking = false;
}

3. 优先使用 IntersectionObserver

对于元素可见性检测,优先使用 IntersectionObserver 而非 scroll 事件。

代码示例

const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) {
            entry.target.classList.add('visible');
        }
    });
}, { threshold: 0.1 });

document.querySelectorAll('.animate-on-scroll').forEach(el => {
    observer.observe(el);
});

4. 避免在滚动处理中读取布局属性

读取 offsetTopgetBoundingClientRect() 等会触发强制重排(forced reflow),应缓存这些值。

代码示例

// 不好:每次滚动都读取
window.addEventListener('scroll', function() {
    const top = element.offsetTop; // 触发重排
});

// 好:缓存布局信息
const elementTop = element.offsetTop;
window.addEventListener('scroll', function() {
    const scrollY = window.scrollY;
    if (scrollY > elementTop) { /* ... */ }
});

5. 使用 CSS scroll-behavior

对于简单的平滑滚动需求,优先使用 CSS scroll-behavior: smooth

代码示例

html {
    scroll-behavior: smooth;
}

6. 无限滚动注意内存

无限滚动会持续添加 DOM 节点,需要注意内存管理。可以使用虚拟滚动或移除不可见的元素。


代码规范示例

规范的滚动处理模块

代码示例

class ScrollHandler {
    constructor(options = {}) {
        this.throttleDelay = options.throttleDelay || 16; // ~60fps
        this.callback = options.callback || null;
        this.ticking = false;
        this.boundHandler = this._handleScroll.bind(this);
    }

    start() {
        window.addEventListener('scroll', this.boundHandler, { passive: true });
        return this;
    }

    stop() {
        window.removeEventListener('scroll', this.boundHandler);
        return this;
    }

    _handleScroll() {
        if (this.ticking) return;
        this.ticking = true;

        requestAnimationFrame(() => {
            if (this.callback) {
                this.callback({
                    scrollY: window.scrollY,
                    scrollX: window.scrollX,
                    direction: this._getScrollDirection(),
                    progress: this._getScrollProgress()
                });
            }
            this.ticking = false;
        });
    }

    _lastScrollY = 0;
    _getScrollDirection() {
        const current = window.scrollY;
        const direction = current > this._lastScrollY ? 'down' : 'up';
        this._lastScrollY = current;
        return direction;
    }

    _getScrollProgress() {
        const docHeight = document.documentElement.scrollHeight - window.innerHeight;
        return docHeight > 0 ? window.scrollY / docHeight : 0;
    }

    scrollToTop(smooth = true) {
        window.scrollTo({
            top: 0,
            behavior: smooth ? 'smooth' : 'auto'
        });
    }

    destroy() {
        this.stop();
        this.callback = null;
    }
}

常见问题与解决方案

问题1:scroll 事件导致页面卡顿

原因:scroll 事件高频触发,处理程序中执行了耗时操作或 DOM 操作。

解决方案:使用 requestAnimationFrame 节流,避免在回调中读取布局属性。

问题2:iOS Safari 的弹性滚动导致 scrollY 异常

原因:iOS Safari 的弹性滚动允许 scrollY 为负值或超过最大值。

解决方案:使用 Math.max(0, window.scrollY) 并检查边界。

问题3:smooth 滚动在部分浏览器不生效

原因:旧版浏览器不支持 scrollTo({ behavior: 'smooth' })

解决方案:使用 polyfill 或自定义平滑滚动函数。

代码示例

function smoothScrollPolyfill(targetY, duration = 500) {
    const startY = window.scrollY;
    const diff = targetY - startY;
    let start = null;

    function step(timestamp) {
        if (!start) start = timestamp;
        const progress = Math.min((timestamp - start) / duration, 1);
        window.scrollTo(0, startY + diff * easeInOutCubic(progress));
        if (progress < 1) requestAnimationFrame(step);
    }

    function easeInOutCubic(t) {
        return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
    }

    requestAnimationFrame(step);
}

问题4:滚动时触发大量重排

原因:在 scroll 回调中读取 offsetTopgetBoundingClientRect() 等触发布局计算的属性。

解决方案:在初始化时缓存布局信息,滚动时只读取 scrollY

问题5:scroll 事件与 IntersectionObserver 选择

原则

  • 需要精确的滚动位置信息 -> 使用 scroll 事件

  • 只需要知道元素是否可见 -> 使用 IntersectionObserver

  • 需要视差效果 -> 使用 scroll 事件 + requestAnimationFrame

  • 无限滚动 -> 优先使用 IntersectionObserver


总结

onscroll 事件是前端交互中最重要的事件之一,但也最容易引发性能问题。以下是核心要点:

  1. 性能优先:始终使用 requestAnimationFrame 节流或 passive: true 优化 scroll 事件处理。

  2. 节流与防抖:节流保证最低执行频率,适合位置检测;防抖减少执行次数,适合滚动结束处理。

  3. IntersectionObserver:对于元素可见性检测,优先使用 IntersectionObserver 替代 scroll 事件。

  4. 滚动位置:使用 window.scrollY 获取页面滚动位置,element.scrollTop 获取元素滚动位置。

  5. 平滑滚动:优先使用 CSS scroll-behavior: smooth,不兼容时使用 JavaScript polyfill。

  6. 避免重排:缓存布局信息,不在 scroll 回调中读取触发重排的属性。

  7. 内存管理:无限滚动场景下注意 DOM 节点数量,考虑虚拟滚动方案。

正确使用 scroll 事件和滚动相关 API,能够在保证性能的前提下实现丰富的滚动驱动交互效果。

常见问题

问题1:scroll 事件导致页面卡顿?

scroll 事件高频触发,处理程序中执行了耗时操作或 DOM 操作。 使用 requestAnimationFrame 节流,避免在回调中读取布局属性。

问题2:iOS Safari 的弹性滚动导致 scrollY 异常?

iOS Safari 的弹性滚动允许 scrollY 为负值或超过最大值。 使用 Math.max(0, window.scrollY) 并检查边界。

问题3:smooth 滚动在部分浏览器不生效?

旧版浏览器不支持 scrollTo({ behavior: 'smooth' })。 使用 polyfill 或自定义平滑滚动函数。

问题4:滚动时触发大量重排?

在 scroll 回调中读取 offsetTopgetBoundingClientRect() 等触发布局计算的属性。 在初始化时缓存布局信息,滚动时只读取 scrollY

问题5:scroll 事件与 IntersectionObserver 选择?

标签: DOM HTML CSS 事件 onscroll

本文由小确幸生活整理发布,转载请注明出处

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: JS集成:HTML onmouseout - 详细教程与实战指南 下一篇: JS集成:HTML onresize 事 - 详细教程与实战指南

poll相关推荐