Skip to content

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最近的可滚动父元素,可能是元素或 windowRef<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); // 60fps

2. 错误处理

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+

相关文档 📖

核心概念

相关 Hooks

实际应用

进阶主题

基於Vant構建的企業級移動端解決方案