React组件中事件监听器函数无法获取更新后的State值问题排查
这个问题的核心是JavaScript闭包特性结合React Hooks的渲染机制导致的,咱们一步步拆解清楚:
为什么onAnswerCall里拿不到更新后的calls?
你在第一个useEffect里注册了answerCall事件监听器,并且依赖项是空数组[]——这意味着这个useEffect只会在组件第一次渲染时执行一次。
而onAnswerCall函数是在组件第一次渲染时创建的,它通过闭包捕获了当时的calls状态(也就是初始的空对象{})。之后不管calls怎么更新,这个已经注册到RNCallKeep的onAnswerCall函数,始终引用的是第一次渲染时的那个calls变量,自然拿不到最新值。
你看到useEffect([calls])和组件根作用域里的console.log能拿到更新后的值,是因为每次calls更新,组件会重新渲染,这两个地方的代码都会重新执行,捕获的是最新的calls;但已经注册的事件监听器里的函数,还是旧的闭包,不会跟着重新渲染更新。
解决方案
这里有两种常用的修复方式,你可以根据场景选择:
方案1:用useRef保存最新状态
useRef的.current属性是可变的,不会受闭包影响,我们可以在calls更新时同步更新ref的值,这样onAnswerCall里直接访问ref.current就能拿到最新的calls:
const callApp = () => { const [calls, setCalls] = useState({}); const callsRef = useRef(calls); // 创建ref // 每次calls更新时同步到ref useEffect(() => { callsRef.current = calls; }, [calls]); const addCall = (key, value) => { setCalls({ ...calls, [key]: value }); }; const displayIncomingCall = number => { const callUUID = getCurrentCallId(); addCall(callUUID, number); // ...其他逻辑 }; const onAnswerCall = () => { // 现在访问callsRef.current就能拿到最新值了 console.log('=== onAnswerCall ::: ===', callsRef.current); }; useEffect(() => { RNCallKeep.addEventListener('answerCall', onAnswerCall); return () => { RNCallKeep.removeEventListener('answerCall', onAnswerCall); }; }, []); // ...其他代码 return (...) }
方案2:让事件监听器随calls更新重新注册
我们可以用useCallback包裹onAnswerCall,把calls加入依赖项,然后在注册监听器的useEffect里也加入onAnswerCall作为依赖——这样每次calls更新,onAnswerCall会重新创建,useEffect也会重新执行,移除旧监听器并注册新的,新的onAnswerCall就能捕获最新的calls:
const callApp = () => { const [calls, setCalls] = useState({}); const addCall = (key, value) => { setCalls({ ...calls, [key]: value }); }; const displayIncomingCall = number => { const callUUID = getCurrentCallId(); addCall(callUUID, number); // ...其他逻辑 }; // 用useCallback包裹,依赖加入calls const onAnswerCall = useCallback(() => { console.log('=== onAnswerCall ::: ===', calls); }, [calls]); useEffect(() => { RNCallKeep.addEventListener('answerCall', onAnswerCall); return () => { RNCallKeep.removeEventListener('answerCall', onAnswerCall); }; }, [onAnswerCall]); // 依赖加入onAnswerCall // ...其他代码 return (...) }
两种方案的对比
- 方案1更适合频繁更新
calls的场景,避免频繁注册/移除事件监听器的开销; - 方案2更符合React Hooks的“依赖驱动更新”的设计思路,逻辑更直观,但如果
calls更新频繁,会带来多次监听器销毁重建的开销。
你可以根据自己的业务场景选择合适的方案~
内容的提问来源于stack exchange,提问作者Balasubramanian

