React多视频自定义进度条异常:播放第二个视频同步更新前一个
问题:React视频进度条更新异常
我正在开发一个React应用,在界面中展示三个视频,每个视频对应一个自定义进度条。目前遇到进度条更新异常问题:第一个视频播放完成时进度条正常,但启动第二个视频播放后,会同时更新第一个和第二个视频的进度条。我使用timeupdate事件来更新视频进度条。
原代码
JavaScript
import * as React from 'react'; import './style.css'; export default function App() { var v3 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4`; var v2 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4`; var v1 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4`; const [id, setId] = React.useState(1); const [currentVideo, setCurrentVideo] = React.useState(''); const [barCompleted, setBarCompleted] = React.useState<any>({ myBar1: false, myBar2: false, myBar3: false, }); const videoRef = React.useRef<any>(); const progress1 = React.useRef<any>(); const progress2 = React.useRef<any>(); const progress3 = React.useRef<any>(); const myBar1 = React.useRef<any>(); const myBar2 = React.useRef<any>(); const myBar3 = React.useRef<any>(); const handleOnEnded = () => { videoRef.current.removeEventListener('timeupdate', () => {}); // setBarCompleted(prev=>{myBar1:true,...prev}) setId((prev) => prev + 1); }; const getVideoById = (id) => { if (id == 1) { setCurrentVideo(v1); } else if (id == 2) { setCurrentVideo(v2); } else if (id == 3) { setCurrentVideo(v3); } }; React.useEffect(() => { getVideoById(id); }, [id]); React.useEffect(() => { videoRef?.current?.load(); configureProgress(id); }, [currentVideo, id]); const configureProgress = (id) => { videoRef?.current?.addEventListener('timeupdate', (e) => { // console.log(e.target.currentTime); let i = 0; if (i == 0) { i = 1; // var elem = document.getElementById('myBar1'); let width = 1; let sid = setInterval(frame, 1000); function frame() { if (videoRef.current.ended || width >= 100) { clearInterval(sid); i = 0; if (id == 1) { progress1.current.style.backgroundColor = 'green'; myBar1.current.style.width = 0 + '%'; } else if (id == 2) { progress2.current.style.backgroundColor = 'green'; myBar2.current.style.width = 0 + '%'; } else if (id == 3) { progress3.current.style.backgroundColor = 'green'; myBar3.current.style.width = 0 + '%'; } } else { width = Math.ceil( (videoRef.current.currentTime / videoRef.current.duration) * 100 ); if (id == 1) { myBar1.current.style.width = width + '%'; console.log('width update id1'); } else if (id == 2) { myBar2.current.style.width = width + '%'; console.log('width update id2'); } else if (id == 3) { myBar3.current.style.width = width + '%'; console.log('width update id3'); } } } } }); }; return ( <div> <div className="progress-container"> <div id="myProgress1" ref={progress1}> <div id="myBar1" ref={myBar1}></div> </div> <div id="myProgress2" ref={progress2}> <div id="myBar2" ref={myBar2}></div> </div> <div id="myProgress3" ref={progress3}> <div id="myBar3" ref={myBar3}></div> </div> </div> {currentVideo && ( <video autoPlay={true} controls muted={true} id="myVideo1" width={300} height={250} ref={videoRef} crossOrigin="anonymous" onEnded={handleOnEnded} preload="metadata" playsInline > <source src={currentVideo} type="video/mp4" /> </video> )} </div> ); }
CSS
.progress-container { display: flex; } #myProgress1, #myProgress2, #myProgress3 { width: 33%; background-color: #ddd; } #myBar1, #myBar2, #myBar3 { width: 1%; height: 30px; background-color: #04aa6d; }
解决方案
问题根源
- 旧监听器未清理:每次切换视频都会新增
timeupdate监听器,旧监听器未被移除,导致多个监听器同时触发更新不同进度条。 - 监听器移除无效:
handleOnEnded中传入的是新匿名函数,和之前添加的监听器不是同一引用,无法完成移除。 - 定时器冗余:
timeupdate事件本身会随播放进度触发,嵌套定时器属于多余逻辑,还会导致进度更新延迟。
修复后的代码
import * as React from 'react'; import './style.css'; export default function App() { const v3 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4`; const v2 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4`; const v1 = `http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerFun.mp4`; const [id, setId] = React.useState(1); const [currentVideo, setCurrentVideo] = React.useState(''); const videoRef = React.useRef<HTMLVideoElement>(null); // 统一管理进度条和进度块的ref const progressRefs = { 1: React.useRef<HTMLDivElement>(null), 2: React.useRef<HTMLDivElement>(null), 3: React.useRef<HTMLDivElement>(null), }; const barRefs = { 1: React.useRef<HTMLDivElement>(null), 2: React.useRef<HTMLDivElement>(null), 3: React.useRef<HTMLDivElement>(null), }; // 保存当前监听器引用,用于移除操作 const timeUpdateListenerRef = React.useRef<((e: Event) => void) | null>(null); const getVideoById = (videoId: number) => { switch(videoId) { case 1: setCurrentVideo(v1); break; case 2: setCurrentVideo(v2); break; case 3: setCurrentVideo(v3); break; default: break; } }; const handleOnEnded = () => { // 移除当前timeupdate监听器 if (videoRef.current && timeUpdateListenerRef.current) { videoRef.current.removeEventListener('timeupdate', timeUpdateListenerRef.current); timeUpdateListenerRef.current = null; } // 标记当前进度条完成 if (progressRefs[id].current) { progressRefs[id].current.style.backgroundColor = 'green'; } // 切换到下一个视频(最多3个) if (id < 3) { setId(prev => prev + 1); } }; // 直接在timeupdate事件中更新进度 const updateProgress = (videoId: number) => { if (!videoRef.current) return; const { currentTime, duration } = videoRef.current; if (duration === 0) return; const width = Math.ceil((currentTime / duration) * 100); if (barRefs[videoId].current) { barRefs[videoId].current.style.width = `${width}%`; } }; React.useEffect(() => { getVideoById(id); }, [id]); React.useEffect(() => { if (!videoRef.current || !currentVideo) return; // 先清理之前的监听器 if (timeUpdateListenerRef.current) { videoRef.current.removeEventListener('timeupdate', timeUpdateListenerRef.current); } // 加载新视频 videoRef.current.load(); // 创建并保存新的监听器 const handleTimeUpdate = () => updateProgress(id); timeUpdateListenerRef.current = handleTimeUpdate; videoRef.current.addEventListener('timeupdate', handleTimeUpdate); // 组件卸载或切换视频时清理监听器 return () => { if (videoRef.current && timeUpdateListenerRef.current) { videoRef.current.removeEventListener('timeupdate', timeUpdateListenerRef.current); timeUpdateListenerRef.current = null; } }; }, [currentVideo, id]); return ( <div> <div className="progress-container"> <div id="myProgress1" ref={progressRefs[1]}> <div id="myBar1" ref={barRefs[1]}></div> </div> <div id="myProgress2" ref={progressRefs[2]}> <div id="myBar2" ref={barRefs[2]}></div> </div> <div id="myProgress3" ref={progressRefs[3]}> <div id="myBar3" ref={barRefs[3]}></div> </div> </div> {currentVideo && ( <video autoPlay={true} controls muted={true} width={300} height={250} ref={videoRef} crossOrigin="anonymous" onEnded={handleOnEnded} preload="metadata" playsInline > <source src={currentVideo} type="video/mp4" /> </video> )} </div> ); }
关键改进
- 统一Ref管理:用对象整合进度条和进度块的ref,简化逻辑调用。
- 监听器引用保存:通过
timeUpdateListenerRef保存当前监听器,确保切换视频时能彻底移除旧监听器。 - 清理副作用:在useEffect的返回函数中添加监听器清理逻辑,避免内存泄漏。
- 简化进度更新:去掉冗余定时器,直接在
timeupdate事件回调中计算并更新进度条,保证实时性。
内容的提问来源于stack exchange,提问作者Jayganesh Gupta
相关产品推荐
相关产品推荐

