useScrollParent 🔍
介绍
在复杂的页面布局中,想要找到元素最近的可滚动父容器吗?useScrollParent 就像一个智能探测器,能够精准定位元素的滚动容器!🎯
无论是实现无限滚动、虚拟列表,还是需要监听特定容器的滚动事件,这个 Hook 都能帮你轻松找到正确的滚动父元素,让你的滚动相关功能更加精准可靠!
代码演示
基础用法 🚀
让我们从一个简单的例子开始,看看如何找到并监听滚动父元素:
html
<template>
<div class="container">
<div class="scroll-area" style="height: 300px; overflow-y: auto;">
<div class="content" style="height: 1000px;">
<div ref="targetElement" class="target">
我是目标元素 🎯
</div>
</div>
</div>
</div>
</template>js
import { ref, watch } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';
export default {
setup() {
const targetElement = ref();
const scrollParent = useScrollParent(targetElement);
// 监听滚动事件
useEventListener(
'scroll',
(event) => {
console.log('滚动容器发生滚动!', {
scrollTop: event.target.scrollTop,
scrollLeft: event.target.scrollLeft
});
},
{ target: scrollParent }
);
// 监听滚动父元素的变化
watch(scrollParent, (newParent, oldParent) => {
console.log('滚动父元素发生变化:', {
old: oldParent,
new: newParent
});
});
return {
targetElement,
scrollParent
};
},
};无限滚动实现 📜
使用 useScrollParent 实现一个智能的无限滚动列表:
html
<template>
<div class="infinite-list" ref="listContainer">
<div
v-for="item in items"
:key="item.id"
class="list-item"
>
{{ item.content }}
</div>
<div v-if="loading" class="loading">
加载中... ⏳
</div>
<div ref="loadTrigger" class="load-trigger"></div>
</div>
</template>js
import { ref, onMounted } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';
export default {
setup() {
const listContainer = ref();
const loadTrigger = ref();
const scrollParent = useScrollParent(listContainer);
const items = ref([]);
const loading = ref(false);
const hasMore = ref(true);
// 加载更多数据
const loadMore = async () => {
if (loading.value || !hasMore.value) return;
loading.value = true;
try {
// 模拟 API 请求
const newItems = await fetchMoreItems();
items.value.push(...newItems);
if (newItems.length < 10) {
hasMore.value = false;
}
} catch (error) {
console.error('加载失败:', error);
} finally {
loading.value = false;
}
};
// 检查是否需要加载更多
const checkLoadMore = () => {
if (!scrollParent.value || !loadTrigger.value) return;
const container = scrollParent.value;
const trigger = loadTrigger.value;
const containerRect = container.getBoundingClientRect();
const triggerRect = trigger.getBoundingClientRect();
// 当触发器进入视口时加载更多
if (triggerRect.top <= containerRect.bottom + 100) {
loadMore();
}
};
// 监听滚动事件
useEventListener('scroll', checkLoadMore, {
target: scrollParent,
passive: true
});
// 初始化数据
onMounted(() => {
loadMore();
});
return {
listContainer,
loadTrigger,
items,
loading
};
}
};虚拟滚动优化 ⚡
结合 useScrollParent 实现高性能的虚拟滚动:
html
<template>
<div
ref="virtualContainer"
class="virtual-scroll-container"
:style="{ height: containerHeight + 'px' }"
>
<div
class="virtual-scroll-content"
:style="{
height: totalHeight + 'px',
transform: `translateY(${offsetY}px)`
}"
>
<div
v-for="item in visibleItems"
:key="item.id"
class="virtual-item"
:style="{ height: itemHeight + 'px' }"
>
{{ item.content }}
</div>
</div>
</div>
</template>js
import { ref, computed, onMounted } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';
export default {
setup() {
const virtualContainer = ref();
const scrollParent = useScrollParent(virtualContainer);
const allItems = ref([]);
const itemHeight = 50;
const containerHeight = 400;
const scrollTop = ref(0);
// 计算可见区域
const visibleCount = Math.ceil(containerHeight / itemHeight) + 2;
const startIndex = computed(() =>
Math.max(0, Math.floor(scrollTop.value / itemHeight) - 1)
);
const endIndex = computed(() =>
Math.min(allItems.value.length, startIndex.value + visibleCount)
);
// 可见项目
const visibleItems = computed(() =>
allItems.value.slice(startIndex.value, endIndex.value)
);
// 总高度
const totalHeight = computed(() =>
allItems.value.length * itemHeight
);
// 偏移量
const offsetY = computed(() =>
startIndex.value * itemHeight
);
// 监听滚动
useEventListener('scroll', (event) => {
scrollTop.value = event.target.scrollTop;
}, {
target: scrollParent,
passive: true
});
// 初始化数据
onMounted(() => {
allItems.value = Array.from({ length: 10000 }, (_, i) => ({
id: i,
content: `虚拟列表项 ${i + 1}`
}));
});
return {
virtualContainer,
visibleItems,
totalHeight,
offsetY,
containerHeight
};
}
};滚动位置同步 🔄
实现多个容器之间的滚动位置同步:
html
<template>
<div class="sync-container">
<div class="left-panel">
<div ref="leftScroll" class="scroll-area">
<div class="content">左侧内容区域</div>
</div>
</div>
<div class="right-panel">
<div ref="rightScroll" class="scroll-area">
<div class="content">右侧内容区域</div>
</div>
</div>
</div>
</template>js
import { ref, nextTick } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';
export default {
setup() {
const leftScroll = ref();
const rightScroll = ref();
const leftScrollParent = useScrollParent(leftScroll);
const rightScrollParent = useScrollParent(rightScroll);
let isLeftScrolling = false;
let isRightScrolling = false;
// 左侧滚动同步到右侧
useEventListener('scroll', async (event) => {
if (isRightScrolling) return;
isLeftScrolling = true;
const { scrollTop, scrollLeft } = event.target;
await nextTick();
if (rightScrollParent.value) {
rightScrollParent.value.scrollTop = scrollTop;
rightScrollParent.value.scrollLeft = scrollLeft;
}
setTimeout(() => {
isLeftScrolling = false;
}, 50);
}, { target: leftScrollParent });
// 右侧滚动同步到左侧
useEventListener('scroll', async (event) => {
if (isLeftScrolling) return;
isRightScrolling = true;
const { scrollTop, scrollLeft } = event.target;
await nextTick();
if (leftScrollParent.value) {
leftScrollParent.value.scrollTop = scrollTop;
leftScrollParent.value.scrollLeft = scrollLeft;
}
setTimeout(() => {
isRightScrolling = false;
}, 50);
}, { target: rightScrollParent });
return {
leftScroll,
rightScroll
};
}
};滚动位置记忆 💾
实现页面刷新后恢复滚动位置:
js
import { ref, onMounted, onUnmounted } from 'vue';
import { useScrollParent, useEventListener } from '@vant/use';
export default {
setup() {
const contentElement = ref();
const scrollParent = useScrollParent(contentElement);
const storageKey = 'scroll-position-memory';
// 保存滚动位置
const saveScrollPosition = () => {
if (!scrollParent.value) return;
const position = {
scrollTop: scrollParent.value.scrollTop,
scrollLeft: scrollParent.value.scrollLeft,
timestamp: Date.now()
};
localStorage.setItem(storageKey, JSON.stringify(position));
};
// 恢复滚动位置
const restoreScrollPosition = () => {
try {
const saved = localStorage.getItem(storageKey);
if (!saved || !scrollParent.value) return;
const position = JSON.parse(saved);
const timeDiff = Date.now() - position.timestamp;
// 只恢复 5 分钟内的滚动位置
if (timeDiff < 5 * 60 * 1000) {
scrollParent.value.scrollTop = position.scrollTop;
scrollParent.value.scrollLeft = position.scrollLeft;
console.log('滚动位置已恢复!📍');
}
} catch (error) {
console.error('恢复滚动位置失败:', error);
}
};
// 监听滚动事件,节流保存
let saveTimer = null;
useEventListener('scroll', () => {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(saveScrollPosition, 300);
}, { target: scrollParent });
// 页面加载时恢复位置
onMounted(() => {
setTimeout(restoreScrollPosition, 100);
});
// 页面卸载时保存位置
onUnmounted(() => {
saveScrollPosition();
});
return {
contentElement
};
}
};API 参考 📚
类型定义
ts
function useScrollParent(
element: Ref<Element | undefined>,
): Ref<Element | Window | undefined>;参数
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| element | 需要查找滚动父元素的目标元素 | Ref<Element | undefined> | - |
返回值
| 参数 | 说明 | 类型 |
|---|---|---|
| scrollParent | 最近的可滚动父元素,可能是元素或 window | Ref<Element | Window | undefined> |
实际应用场景 🎯
1. 无限滚动列表
- 新闻列表: 用户滚动到底部自动加载更多新闻
- 商品展示: 电商网站的商品列表无限加载
- 社交动态: 朋友圈、微博等社交内容的无限滚动
2. 虚拟滚动优化
- 大数据表格: 处理成千上万行数据的表格
- 聊天记录: 长时间的聊天记录虚拟滚动
- 文件列表: 大量文件的高性能展示
3. 滚动同步
- 代码编辑器: 左右面板滚动同步
- 对比工具: 文档对比时的同步滚动
- 双语阅读: 中英文对照阅读的滚动同步
4. 滚动位置管理
- 阅读进度: 记住用户的阅读位置
- 表单填写: 长表单的滚动位置记忆
- 搜索结果: 返回搜索页面时恢复滚动位置
最佳实践 💡
1. 性能优化
js
// ✅ 推荐:使用 passive 监听器
useEventListener('scroll', handleScroll, {
target: scrollParent,
passive: true
});
// ✅ 推荐:使用节流避免频繁触发
import { throttle } from 'lodash-es';
const throttledHandler = throttle(handleScroll, 16); // 60fps2. 错误处理
js
// ✅ 推荐:检查滚动父元素是否存在
const handleScroll = () => {
if (!scrollParent.value) {
console.warn('滚动父元素不存在');
return;
}
// 处理滚动逻辑
};3. 内存管理
js
// ✅ 推荐:组件卸载时清理定时器
onUnmounted(() => {
if (scrollTimer) {
clearTimeout(scrollTimer);
}
});4. 响应式处理
js
// ✅ 推荐:监听滚动父元素变化
watch(scrollParent, (newParent, oldParent) => {
if (oldParent) {
// 清理旧的事件监听器
}
if (newParent) {
// 添加新的事件监听器
}
});调试技巧 🔧
1. 检查滚动父元素
js
// 在控制台查看滚动父元素信息
watch(scrollParent, (parent) => {
console.log('滚动父元素:', {
element: parent,
tagName: parent?.tagName,
className: parent?.className,
scrollHeight: parent?.scrollHeight,
clientHeight: parent?.clientHeight
});
}, { immediate: true });2. 监听滚动事件
js
// 调试滚动事件触发情况
useEventListener('scroll', (event) => {
console.log('滚动事件:', {
scrollTop: event.target.scrollTop,
scrollLeft: event.target.scrollLeft,
timestamp: Date.now()
});
}, { target: scrollParent });3. 可视化滚动区域
css
/* 临时添加边框来可视化滚动容器 */
.debug-scroll-parent {
border: 2px solid red !important;
background: rgba(255, 0, 0, 0.1) !important;
}浏览器兼容性 🌐
useScrollParent 使用标准的 DOM API,支持所有现代浏览器:
- Chrome 60+
- Firefox 55+
- Safari 12+
- Edge 79+
相关文档 📖
核心概念
- Element.getBoundingClientRect() - 获取元素位置信息
- 滚动事件 - 了解滚动事件机制
- Intersection Observer API - 元素可见性检测
相关 Hooks
- useEventListener - 事件监听管理
- useRect - 元素位置和尺寸获取
- useWindowSize - 窗口尺寸监听