IntersectionObserver回调在目标未进入视口时仍触发问题排查
我希望元素进入视口时执行函数,但 IntersectionObserver 的回调在元素远低于视口时就已触发,甚至初始化时就触发了。以下是相关代码:
App.jsx
import { useState, useEffect, useRef } from 'react'; import Rect from "./Rect.jsx" export function App(props) { const [rects, setRects] = useState([]) const refer = useRef(null) const observer = useRef() useEffect(() => { for (let i = 0; i < 50; i++) { (i !== 40) ? setRects((reacts) => [...reacts, <Rect index={i} />]) : setRects((reacts) => [...reacts, <Rect index={i} ref={refer} />]) } observer.current = new IntersectionObserver((data) => { alert(data[0].isIntersecting) }, { threshold: 1 }) }, []) // 文档里的正确写法 // observer.current=new IntersectionObserver((data)=>{ // if (data[0].isIntersecting) // alert(data[0].isIntersecting) // },{threshold:1}) useEffect(() => { if (refer.current) { observer.current.observe(refer.current) } }) // },[] // 加空数组的话,元素可见时回调也不触发 return ( <div className='App'> <h1>Hello React.</h1> {rects.map((rect) => rect)} </div> ); }
Rect.jsx
import { forwardRef } from "react"; const Rect = forwardRef((props, ref) => { return (props.index == 40) ? <div style={{ backgroundColor: "red", height: "20px", width: "20px" }} ref={ref}> </div> : <div style={{ backgroundColor: "black", height: "20px", width: "20px" }}> </div> }) export default Rect
疑问:为什么初始化时回调就会触发,此时并没有元素发生相交?
原因解析
1. IntersectionObserver 的默认行为:初始状态同步
IntersectionObserver 设计时就会在调用 observe() 方法后立即触发一次回调,目的是同步当前元素的相交状态——不管元素此时在视口内还是外,它都会返回当前的 isIntersecting 值。你看到的初始化回调,其实是在告诉你「当前元素不在视口内,isIntersecting 为 false」,只是你的代码没做判断,直接弹出了这个值,才会误以为是元素进入了视口。
2. 第二个 useEffect 的执行时机问题
你第二个 useEffect 没有加依赖数组,这意味着每次组件重渲染都会执行它。当红色矩形(index=40)完成挂载后,refer.current 被赋值,此时这个 useEffect 执行并调用 observe(),触发了上述的初始回调(此时元素在视口外,isIntersecting=false)。
如果给这个 useEffect 加空数组 [],它只会在组件首次渲染时执行一次,但此时 refer.current 还未被赋值(React 还没完成 Rect 组件的挂载,ref 还没绑定到 DOM 元素),所以 observe() 根本没被调用,自然不会触发任何回调。
修正方案
方案一:过滤非相交状态的回调
这就是你注释里的正确写法——只在 isIntersecting 为 true 时执行逻辑,忽略初始的「不相交」回调和元素离开视口的回调:
observer.current = new IntersectionObserver((data) => { if (data[0].isIntersecting) { alert("元素进入视口了"); } }, { threshold: 1 })
方案二:正确监听 ref 的挂载时机
调整第二个 useEffect,让它只在 refer.current 变化时执行,确保 DOM 元素存在后再调用 observe():
useEffect(() => { if (refer.current) { observer.current.observe(refer.current); // 组件卸载时停止观察,避免内存泄漏 return () => observer.current.unobserve(refer.current); } }, [refer.current])
额外优化:批量创建元素
你当前用循环多次调用 setRects 会导致组件多次重渲染,建议改成批量创建后一次性设置:
useEffect(() => { const newRects = []; for (let i = 0; i < 50; i++) { newRects.push(i !== 40 ? <Rect index={i} /> : <Rect index={i} ref={refer} />); } setRects(newRects); observer.current = new IntersectionObserver((data) => { if (data[0].isIntersecting) { alert("元素进入视口了"); } }, { threshold: 1 }); }, []);
内容的提问来源于stack exchange,提问作者suyog

