一、核心原理
react-intersection-observer
的核心是通过浏览器原生的 Intersection Observer
API 监听目标元素与视口的交叉状态变化,并将状态通过 React Hook 暴露给组件。
关键点:
-
Intersection Observer API
- 浏览器提供的异步 API,用于监听目标元素与祖先元素或视口的交叉状态
- 通过
threshold
和rootMargin
控制触发条件
-
Hook 职责
- 创建/销毁 Observer 实例
- 管理目标元素的引用(ref)
- 将交叉状态(
inView
)和观察器配置暴露给组件 - 处理组件卸载时的清理工作
-
性能优化
- 复用同一个 Observer 实例(多个目标元素共享)
- 避免不必要的重新渲染
二、简化版 TypeScript 实现
tsx
// useInView.ts
import {
useState,
useEffect,
useRef,
MutableRefObject,
RefObject,
Ref,
} from 'react';
// 交叉观测的选项
type IntersectionObserverOptions = {
root?: Element | Document | null;
rootMargin?: string;
threshold?: number | number[];
};
// 返回的类型
type UseInViewReturn = {
inView: boolean;
ref: RefObject<Element>;
entry?: IntersectionObserverEntry;
};
// 自定义useInView hook实现
export function useInView(
options: IntersectionObserverOptions = {}
): UseInViewReturn {
// 定义状态 用于表示是否有交叉
const [inView, setInView] = useState<boolean>(false);
// 与视口交叉的view数组
const [entry, setEntry] = useState<IntersectionObserverEntry | undefined>();
const observedElement = useRef<Element | null>(null);
// 存储 创建的IntersectionObserver实例
const observerRef = useRef<IntersectionObserver | null>(null);
// 创建 Intersection Observer 实例
const createObserver = () => {
// 销毁旧的 Observer(避免重复观察
if (observerRef.current) {
observerRef.current.disconnect();
}
// 创建新的 Observer
observerRef.current = new IntersectionObserver(
(entries) => {
// 遍历所有交叉变化(通常只有一个元素)
entries.forEach((entry) => {
setInView(entry.isIntersecting);
setEntry(entry);
});
},
options
);
};
// 处理目标元素变化
const handleRefChange = (node: Element | null) => {
if (observedElement.current) {
// 如果已有观察的元素,先取消观察
observerRef.current?.unobserve(observedElement.current);
}
observedElement.current = node;
if (node && observerRef.current) {
// 开始观察新元素
observerRef.current.observe(node);
} else if (!node && observerRef.current) {
// 如果元素被移除,取消观察
observerRef.current.disconnect();
}
};
useEffect(() => {
// 创建监听器
createObserver();
return () => {
// 组件卸载时清理 Observer
observerRef.current?.disconnect();
};
}, [options.root, options.rootMargin, options.threshold]);
useEffect(() => {
if (observedElement.current && observerRef.current) {
// 当元素变化时重新观察
observerRef.current.observe(observedElement.current);
}
}, [observedElement.current]);
return {
inView,
ref: {
current: observedElement.current,
// 兼容 ref 回调和 ref 对象两种用法
...(typeof useRef === 'function' ? { ref: handleRefChange } : {}),
} as RefObject<Element>,
entry,
};
}
// 更友好的 ref 返回类型版本
export function useInViewWithRefCallback(
options: IntersectionObserverOptions = {}
): [RefCallback<Element>, boolean, IntersectionObserverEntry | undefined] {
const [inView, setInView] = useState<boolean>(false);
const [entry, setEntry] = useState<IntersectionObserverEntry | undefined>();
const observerRef = useRef<IntersectionObserver | null>(null);
useEffect(() => {
observerRef.current = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
setInView(entry.isIntersecting);
setEntry(entry);
});
},
options
);
return () => {
observerRef.current?.disconnect();
};
}, [options]);
const refCallback: RefCallback<Element> = (node) => {
if (observerRef.current) {
if (node) {
observerRef.current.observe(node);
} else {
observerRef.current.disconnect();
}
}
};
return [refCallback, inView, entry];
}
三、使用示例
版本1:使用 ref
对象
tsx
import React, { useRef } from 'react';
import { useInView } from './useInView';
const MyComponent = () => {
const { ref, inView } = useInView({
threshold: 0.5,
});
return (
<div ref={ref}>
{inView ? 'Visible' : 'Hidden'}
</div>
);
};
版本2:使用 ref 回调(更灵活)
tsx
import React from 'react';
import { useInViewWithRefCallback } from './useInView';
const MyComponent = () => {
const [ref, inView] = useInViewWithRefCallback();
return (
<div ref={ref}>
{inView ? 'Visible' : 'Hidden'}
</div>
);
};
四、实现原理详解
-
Observer 管理
- 使用
useRef
保存 Observer 实例,避免重复创建 - 在
useEffect
中初始化 Observer,并在组件卸载时清理
- 使用
-
元素观察
- 通过
handleRefChange
处理目标元素的变化 - 当元素变化时,先取消旧元素的观察,再开始观察新元素
- 通过
-
状态更新
- Observer 回调中通过
setInView
和setEntry
更新状态 - 状态变化会触发组件重新渲染
- Observer 回调中通过
-
性能优化
- 共享同一个 Observer 实例(多个 Hook 调用共享配置)
- 避免在每次渲染时重新创建 Observer
五、与官方实现的差异
简化版实现与官方库的主要区别:
特性 | 简化版实现 | 官方实现 |
---|---|---|
多元素支持 | ❌ 不支持 | ✅ 支持多个元素共享 Observer |
高级配置 | 基础配置 | 支持更多配置选项 |
类型安全 | 基础类型 | 完善的 TypeScript 类型 |
性能优化 | 基础优化 | 更复杂的优化策略 |
错误处理 | 无 | 完善的错误边界处理 |
六、关键 TypeScript 类型说明
ts
type IntersectionObserverOptions = {
root?: Element | Document | null; // 观察的根元素
rootMargin?: string; // 类似 CSS margin 的偏移量
threshold?: number | number[]; // 触发回调的可见比例阈值
};
type UseInViewReturn = {
inView: boolean; // 是否在视口中
ref: RefObject<Element>; // 目标元素的 ref
entry?: IntersectionObserverEntry; // 最新的交叉状态信息
};
interface IntersectionObserverEntry {
boundingClientRect: DOMRectReadOnly;
intersectionRatio: number;
intersectionRect: DOMRectReadOnly;
isIntersecting: boolean; // 是否正在交叉
rootBounds: DOMRectReadOnly | null;
target: Element;
time: number;
}
这个简化实现展示了 react-intersection-observer
的核心原理,实际生产环境中的官方实现会更复杂(包括多元素支持、性能优化等),但已经能够满足基本的视口检测需求。