清除Timeout并卸载组件后,Timeout仍持续触发执行的问题
问题:公交标记动画无法停止,清除Timeout/卸载组件后仍持续触发
我基于坐标数组实现公交标记动画,用setTimeout触发函数让标记移动到下一个坐标。父组件切换站点时,会先把routes设为null,再获取新站点的公交坐标,此时需要停止当前动画。但目前即使清除Timeout、卸载组件,动画函数仍会持续触发,找不到问题所在。
相关代码
Bus Marker组件
import React from 'react'; import { Marker } from 'react-native-yamap'; import { BusView } from '../../../../../../components/transport/bus-view'; import { Route } from '../../../../../../modules/transport/types/Route'; import { Station } from 'modules/transport/types/Station'; interface Props { route: Route; visible: boolean; selectedStation: Station | null; routes: Route[]; } export const BusMarker = React.memo(function BusMarker(props: Props) { const name = props.route?.bus_number?.replace(/\D/g, ''); const busRef = React.useRef<Marker>(null); const [timeouts, setTimeOuts] = React.useState<NodeJS.Timeout[]>([]); let timerRef = React.createRef(); const animateMarker = React.useCallback(() => { const current = props?.route.current_location ?? 0; let prevTimeOuts: NodeJS.Timeout[] = []; console.log('animation start for marker', props.route.bus_number, 'from position :', current); props?.route?.route?.slice(current + 1)?.forEach((bus, index) => { timerRef.current = setTimeout(() => { console.log('animateMarker', props.route.bus_number, 'to position index:', index); busRef?.current?.animatedMoveTo( { lat: bus.lat, lon: bus.lon, }, 1000 * props.route.average_second_per_coordinate, ); }, index * 1000 * props.route.average_second_per_coordinate); prevTimeOuts = [...prevTimeOuts, timerRef.current]; }); setTimeOuts(prevTimeOuts); }, [props.selectedStation]); const clearAllTimeouts = React.useCallback(() => { console.log('clearAllTimeouts'); timeouts.forEach(timeout => { clearTimeout(timeout.current); }); setTimeOuts([]); }, [timeouts]); React.useEffect(() => { animateMarker(); }, []); React.useEffect(() => { return () => { clearAllTimeouts(); }; }, [props.selectedStation, props.routes]); return ( props.route?.route?.[0] && ( <Marker visible={props.visible} scale={1} ref={busRef} key={props.route.bus_number} point={{ lat: props.route?.route[props.route.current_location ?? 0].lat, lon: props.route?.route[props.route.current_location ?? 0].lon, }} children={<BusView name={name} type={props.route?.tt_id} />} /> ) ); });
父组件代码
React.useEffect(() => { getRoutes(); // 获取所选站点的公交新坐标 }, [selectedStation]); const handleChangeStation = (station: Station) => { setRoutes(() => null); setSelectedStation(() => station); }; {routes?.[0] && routes.map((route, index) => ( <BusMarker key={index} route={route} visible={showBuses} selectedStation={selectedStation} routes={routes} /> ))} // 在地图上渲染公交标记
问题原因分析
- timerRef使用错误:用
React.createRef()保存timeout ID,但createRef在组件每次渲染时都会生成新的引用,导致部分timeout ID丢失,无法被正确清除。 - clearAllTimeouts逻辑错误:
timeouts数组中存储的是setTimeout返回的ID(NodeJS.Timeout),但代码里错误地调用了timeout.current,实际应该直接传入timeout ID执行clearTimeout。 - useCallback依赖缺失:
animateMarker的依赖只加了props.selectedStation,但函数内部用到了props.route的多个属性(current_location、bus_number、route数组、average_second_per_coordinate),这些未被加入依赖的属性会导致函数捕获旧的props值,即使props更新,动画仍会沿用旧数据。 - useEffect依赖错误:第一个
useEffect依赖为空数组,只会执行一次初始化动画,当props.route变化时不会重新触发;同时清理逻辑的依赖未包含clearAllTimeouts,可能导致清理函数使用旧的timeouts数组。 - 父组件key值不合理:用
index作为BusMarker的key,当routes切换时,key可能重复,导致React复用旧组件而非卸载,旧的timeout因此继续运行。
解决方案
1. 修正timerRef的使用
将React.createRef()改为React.useRef(),确保组件生命周期内引用唯一:
const timerRef = React.useRef<NodeJS.Timeout | null>(null);
2. 修复clearAllTimeouts的逻辑
直接使用timeout ID执行清除:
const clearAllTimeouts = React.useCallback(() => { console.log('clearAllTimeouts'); timeouts.forEach(timeout => { clearTimeout(timeout); }); setTimeOuts([]); }, [timeouts]);
3. 补全animateMarker的依赖项
将所有用到的props属性加入useCallback依赖,或者直接依赖props.route:
const animateMarker = React.useCallback(() => { // ... 原有逻辑 }, [props.route, props.selectedStation]);
4. 调整useEffect的触发与清理逻辑
让动画在props.route变化时重新执行,同时确保清理函数能获取最新的clearAllTimeouts:
React.useEffect(() => { animateMarker(); return () => { clearAllTimeouts(); }; }, [animateMarker, clearAllTimeouts]);
5. 父组件使用唯一key值
替换index为公交的唯一标识,比如route.bus_number,确保切换站点时旧组件被正确卸载:
{routes?.[0] && routes.map((route) => ( <BusMarker key={route.bus_number} route={route} visible={showBuses} selectedStation={selectedStation} routes={routes} /> ))}
6. 增加挂载状态防止卸载后执行操作
添加一个状态标记组件是否挂载,避免卸载后执行DOM操作或setState:
const isMounted = React.useRef(true); React.useEffect(() => { return () => { isMounted.current = false; clearAllTimeouts(); }; }, [clearAllTimeouts]); // 在animateMarker的setTimeout回调中判断 timerRef.current = setTimeout(() => { if (!isMounted.current) return; // ... 原有动画逻辑 }, ...);
内容的提问来源于stack exchange,提问作者Zero0
相关产品推荐
相关产品推荐

