ByAI:react-intersection-observer 中useInView的ts简要实现

一、核心原理

react-intersection-observer 的核心是通过浏览器原生的 Intersection Observer API 监听目标元素与视口的交叉状态变化,并将状态通过 React Hook 暴露给组件。

关键点:

  1. Intersection Observer API

    • 浏览器提供的异步 API,用于监听目标元素与祖先元素或视口的交叉状态
    • 通过 thresholdrootMargin 控制触发条件
  2. Hook 职责

    • 创建/销毁 Observer 实例
    • 管理目标元素的引用(ref)
    • 将交叉状态(inView)和观察器配置暴露给组件
    • 处理组件卸载时的清理工作
  3. 性能优化

    • 复用同一个 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>
  );
};

四、实现原理详解

  1. Observer 管理

    • 使用 useRef 保存 Observer 实例,避免重复创建
    • useEffect 中初始化 Observer,并在组件卸载时清理
  2. 元素观察

    • 通过 handleRefChange 处理目标元素的变化
    • 当元素变化时,先取消旧元素的观察,再开始观察新元素
  3. 状态更新

    • Observer 回调中通过 setInViewsetEntry 更新状态
    • 状态变化会触发组件重新渲染
  4. 性能优化

    • 共享同一个 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 的核心原理,实际生产环境中的官方实现会更复杂(包括多元素支持、性能优化等),但已经能够满足基本的视口检测需求。

相关推荐
谦谦橘子3 个月前
preact原理解析
前端·javascript·preact
乐予吕7 个月前
JavaScript 信号:如何将响应式功能带到普通 Web 开发中
前端·javascript·preact
超能996要躺平1 年前
使用 react-captcha-code 在 React 应用中实现验证码功能
前端·preact
summo2 年前
SpringBoot + 通义千问 + 自定义React组件,支持EventStream数据解析!
人工智能·openai·preact
ToolkitUse2 年前
🎉🎉🎉一文全面了解:一个神奇的 react-antd-admin mobx 设计
前端·react.js·preact
ToolkitUse2 年前
🎉🎉🎉一文全面了解:一个神奇的 react-antd-admin 动态路由
前端·react.js·preact
ToolkitUse2 年前
🎉🎉🎉一文全面了解:一个神奇的 react-antd-admin 动态菜单
前端·react.js·preact
ToolkitUse2 年前
🎉🎉🎉一文全面了解:一个神奇的 react-antd-admin 鉴权方式
前端·react.js·preact
程序员王天2 年前
React+node 图片剪裁上传,集成本地存储和阿里云OSS
前端·preact