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"<div 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 - ウィンドウサイズの監視
実際のアプリケーション
- 仮想スクロールの実装 - Vue 仮想スクロールコンポーネント
- 無限スクロールコンポーネント - Vant List コンポーネント
- スクロールコンテナ - スクロールビューコンポーネント
高度なトピック
- パフォーマンス最適化 - 長いタスクの最適化
- 仮想化技術 - 大規模データレンダリングの最適化
- スクロールパフォーマンス - スクロールイベントの最適化