pin_drop当前位置:知识文库 ❯ 图文
JS集成:HTML onscroll 事 - 详细教程与实战指南
教程简介
onscroll 事件在元素内容滚动时触发,是实现滚动驱动交互的核心事件。它广泛应用于无限滚动加载、滚动动画、回到顶部按钮、导航栏固定、视差效果等场景。然而,scroll 事件也是最容易引发性能问题的事件之一,因为它在滚动过程中会高频触发,如果不加以优化,可能导致页面卡顿。
本教程将深入讲解 onscroll 事件的核心概念、性能问题与优化策略(节流与防抖)、滚动位置获取、滚动到指定位置、无限滚动实现以及滚动动画效果。
核心概念
onscroll 事件定义
scroll 事件在以下情况触发:
用户通过鼠标滚轮滚动页面
用户通过拖拽滚动条滚动
用户通过键盘(方向键、Page Up/Down、Space)滚动
JavaScript 代码通过
scrollTo()、scrollIntoView()等方法触发滚动
scroll 事件的关键特性
滚动位置属性
节流(Throttle)与防抖(Debounce)
语法与用法
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>浏览器兼容性
注意事项与最佳实践
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. 避免在滚动处理中读取布局属性
读取 offsetTop、getBoundingClientRect() 等会触发强制重排(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 回调中读取 offsetTop、getBoundingClientRect() 等触发布局计算的属性。
解决方案:在初始化时缓存布局信息,滚动时只读取 scrollY。
问题5:scroll 事件与 IntersectionObserver 选择
原则:
需要精确的滚动位置信息 -> 使用 scroll 事件
只需要知道元素是否可见 -> 使用 IntersectionObserver
需要视差效果 -> 使用 scroll 事件 + requestAnimationFrame
无限滚动 -> 优先使用 IntersectionObserver
总结
onscroll 事件是前端交互中最重要的事件之一,但也最容易引发性能问题。以下是核心要点:
性能优先:始终使用
requestAnimationFrame节流或passive: true优化 scroll 事件处理。节流与防抖:节流保证最低执行频率,适合位置检测;防抖减少执行次数,适合滚动结束处理。
IntersectionObserver:对于元素可见性检测,优先使用 IntersectionObserver 替代 scroll 事件。
滚动位置:使用
window.scrollY获取页面滚动位置,element.scrollTop获取元素滚动位置。平滑滚动:优先使用 CSS
scroll-behavior: smooth,不兼容时使用 JavaScript polyfill。避免重排:缓存布局信息,不在 scroll 回调中读取触发重排的属性。
内存管理:无限滚动场景下注意 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 回调中读取 offsetTop、getBoundingClientRect() 等触发布局计算的属性。 在初始化时缓存布局信息,滚动时只读取 scrollY。
问题5:scroll 事件与 IntersectionObserver 选择?
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别