如何检测最近进入视口的React函数式视频组件?
解决函数式视频组件的视口追踪问题
核心问题分析
你当前代码存在几个关键问题:
- 函数式组件
VideoContainer未转发ref,导致父组件的ref1/ref2/ref3无法获取真实DOM节点 useIsInViewport在组件顶层直接调用,初始渲染时ref.current为null,IntersectionObserver无法正常工作- 用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
相关产品推荐
相关产品推荐

