React中Promise为何无法获取外部isPlayingClips状态的更新?
背景说明(便于理解问题)
我有一个多维数组,存储了要播放的视频片段的开始和结束时间。我遍历这些时间,播放视频并等待其到达结束时间(通过Promise实现),此时resolve Promise即可播放下一段视频。
代码中失效的部分是:Promise内部有一个if语句持续检查isPlayingClips状态,当状态变为false时,应提前resolve Promise并返回"cancelled"。我能看到useEffect内的状态已更新,但Promise内的状态从未变化。
示例代码
以下是整个依赖isPlayingClips状态的useEffect:
const [isPlayingClips, setIsPlayingClips] = useState<boolean>(false); useEffect(() => { // 这里能正确打印状态更新,但Promise内部看不到变化 console.log("isPlayingClips updated:", isPlayingClips); async function playClips() { // 返回类似这样的数组: [[57.98, 62.98], [114.96, 119.96]] let clips = getAllClips(); for (let i = 0, n = clips.length; i < n; ++i) { // 片段的开始和结束时间 let start = clips[i][0]; let end = clips[i][1]; // 跳转到片段开始位置并播放 updateVideoTime(start); player.play(); // 播放片段直到到达结束时间或播放被取消 let cp = await new Promise((resolve) => { // isPlayingClips始终是Promise创建时的true值,状态变为false时这里也不会更新 const u = () => { console.log(player.currentTime, end, isPlayingClips); // 如果isPlayingClips变为false,提前resolve Promise if (!isPlayingClips) { console.log("CANCELLING PLAYING CLIPS"); player.removeEventListener("timeupdate", u); resolve("cancelled"); } // 到达结束时间时resolve Promise if (player.currentTime >= end) { player.pause(); player.removeEventListener("timeupdate", u); resolve("finished"); } }; player.addEventListener("timeupdate", u); }); // 如果Promise被取消,停止继续播放后续片段 if (cp == "cancelled") return; } } if (isPlayingClips) { playClips(); } else { player.pause(); } }, [isPlayingClips]);
问题
我的写法是否有误?为何状态变更无法在Promise内部体现?
解决方案
问题出在闭包捕获了状态的旧值:当useEffect因为isPlayingClips更新重新执行时,之前启动的playClips函数是在旧的渲染周期中创建的,它捕获的isPlayingClips是当时的固定值(初始为true),后续状态更新后,这个函数内部的变量不会同步更新。
修复步骤:
- 使用
useRef跟踪最新的状态值,因为ref的current属性是可变的,不会被闭包锁定,能跨渲染周期保持最新状态。 - 在
useEffect中同步ref与状态值。 - 在事件监听函数中使用ref的
current属性获取最新状态。 - 添加清理函数,避免残留的事件监听导致内存泄漏或错误。
修改后的代码:
const [isPlayingClips, setIsPlayingClips] = useState<boolean>(false); // 创建ref跟踪最新状态 const isPlayingClipsRef = useRef(isPlayingClips); // 同步ref与状态值 useEffect(() => { isPlayingClipsRef.current = isPlayingClips; }, [isPlayingClips]); useEffect(() => { console.log("isPlayingClips updated:", isPlayingClips); let currentListener: (() => void) | null = null; async function playClips() { let clips = getAllClips(); for (let i = 0, n = clips.length; i < n; ++i) { let start = clips[i][0]; let end = clips[i][1]; updateVideoTime(start); player.play(); let cp = await new Promise((resolve) => { currentListener = () => { console.log(player.currentTime, end, isPlayingClipsRef.current); // 使用ref.current获取最新状态 if (!isPlayingClipsRef.current) { console.log("CANCELLING PLAYING CLIPS"); player.removeEventListener("timeupdate", currentListener!); resolve("cancelled"); } if (player.currentTime >= end) { player.pause(); player.removeEventListener("timeupdate", currentListener!); resolve("finished"); } }; player.addEventListener("timeupdate", currentListener); }); if (cp == "cancelled") return; } } if (isPlayingClips) { playClips(); } else { player.pause(); // 清理当前的事件监听 if (currentListener) { player.removeEventListener("timeupdate", currentListener); } } // 组件卸载或状态更新时清理监听 return () => { if (currentListener) { player.removeEventListener("timeupdate", currentListener); } }; }, [isPlayingClips]);
额外说明:
- 用
useRef保存状态是解决React闭包陷阱的常用方案,它能让你在异步操作或事件监听中获取最新的状态值。 - 清理函数的作用是在
useEffect重新执行或组件卸载时,移除之前添加的事件监听,避免多个监听同时存在导致的逻辑混乱和内存泄漏。
内容的提问来源于stack exchange,提问作者spindi598
相关产品推荐
相关产品推荐

