You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何检测最近进入视口的React函数式视频组件?

解决函数式视频组件的视口追踪问题

核心问题分析

你当前代码存在几个关键问题:

  1. 函数式组件VideoContainer未转发ref,导致父组件的ref1/ref2/ref3无法获取真实DOM节点
  2. useIsInViewport在组件顶层直接调用,初始渲染时ref.current为null,IntersectionObserver无法正常工作
  3. 用Proxy监听状态变化的方式不合适,会触发不必要的渲染循环,且无法准确追踪「最近进入视口」的顺序

步骤1:给VideoContainer添加ref转发

函数式组件默认不接收ref,必须用React.forwardRef包裹,才能让父组件拿到内部DOM元素:

const VideoContainer = React.forwardRef((props, ref) => {
  // 将ref绑定到视频容器的根DOM元素上(比如外层div或video标签)
  return (
    <div ref={ref} className="video-container">
      <video src={props.videoSource} muted={props.isGlobalMute} />
      {/* 其他组件内容 */}
    </div>
  );
});

步骤2:重构useIsInViewport钩子

调整钩子逻辑,确保仅在ref存在时启动监听,同时返回进入视口的时间戳,方便判断最新视频:

function useIsInViewport(ref) {
  const [state, setState] = useState({
    isIntersecting: false,
    lastEnterTime: 0 // 记录进入视口的时间戳
  });

  const observer = useMemo(() => {
    return new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setState({
          isIntersecting: true,
          lastEnterTime: Date.now()
        });
      } else {
        setState(prev => ({ ...prev, isIntersecting: false }));
      }
    });
  }, []);

  useEffect(() => {
    const currentElement = ref.current;
    if (currentElement) {
      observer.observe(currentElement);
    }

    return () => {
      if (currentElement) {
        observer.unobserve(currentElement);
      }
      observer.disconnect();
    };
  }, [ref, observer]);

  return state;
}

步骤3:父组件重新实现视口追踪逻辑

去掉Proxy,改用useEffect监听所有视频的视口状态变化,通过时间戳判断最近进入的视频:

function App() {
  const ref1 = useRef(null);
  const ref2 = useRef(null);
  const ref3 = useRef(null);

  const [isGlobalMute, setIsGlobalMute] = useState(true);
  const [mostRecentVideo, setMostRecentVideo] = useState("");

  // 获取每个视频的视口状态
  const podcast1State = useIsInViewport(ref1);
  const podcast2State = useIsInViewport(ref2);
  const podcast3State = useIsInViewport(ref3);

  // 监听状态变化,更新最近的视频
  useEffect(() => {
    const videoEntries = [
      { title: "Podcast 1", ...podcast1State },
      { title: "Podcast 2", ...podcast2State },
      { title: "Podcast 3", ...podcast3State },
    ];

    // 筛选当前在视口的视频,按进入时间倒序取第一个
    const visibleVideos = videoEntries.filter(v => v.isIntersecting);
    if (visibleVideos.length > 0) {
      const latest = visibleVideos.sort((a, b) => b.lastEnterTime - a.lastEnterTime)[0];
      setMostRecentVideo(latest.title);
    }
  }, [podcast1State, podcast2State, podcast3State]);

  return (
    <div className="app">
      <VideoContainer
        ref={ref1}
        title="Podcast 1"
        isGlobalMute={isGlobalMute}
        setIsGlobalMute={setIsGlobalMute}
        videoSource={video1}
        {/* 其他props */}
      />
      <VideoContainer
        ref={ref2}
        title="Podcast 2"
        isGlobalMute={isGlobalMute}
        setIsGlobalMute={setIsGlobalMute}
        videoSource={video2}
        {/* 其他props */}
      />
      <VideoContainer
        ref={ref3}
        title="Podcast 3"
        isGlobalMute={isGlobalMute}
        setIsGlobalMute={setIsGlobalMute}
        videoSource={video3}
        {/* 其他props */}
      />
    </div>
  );
}

额外优化建议

  • 若视频数量较多,可将视频数据整理为数组,用map循环渲染,避免重复代码
  • 给IntersectionObserver配置threshold参数(如threshold: 0.5),让视频显示一半时才判定为进入视口,更符合实际场景
  • 可指定IntersectionObserver的root参数,限制视口检测的范围

内容的提问来源于stack exchange,提问作者CrumrineCoder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 04:45:41