You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

清除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} 
      />
 ))} // 在地图上渲染公交标记

问题原因分析

  1. timerRef使用错误:用React.createRef()保存timeout ID,但createRef在组件每次渲染时都会生成新的引用,导致部分timeout ID丢失,无法被正确清除。
  2. clearAllTimeouts逻辑错误:timeouts数组中存储的是setTimeout返回的ID(NodeJS.Timeout),但代码里错误地调用了timeout.current,实际应该直接传入timeout ID执行clearTimeout。
  3. useCallback依赖缺失:animateMarker的依赖只加了props.selectedStation,但函数内部用到了props.route的多个属性(current_location、bus_number、route数组、average_second_per_coordinate),这些未被加入依赖的属性会导致函数捕获旧的props值,即使props更新,动画仍会沿用旧数据。
  4. useEffect依赖错误:第一个useEffect依赖为空数组,只会执行一次初始化动画,当props.route变化时不会重新触发;同时清理逻辑的依赖未包含clearAllTimeouts,可能导致清理函数使用旧的timeouts数组。
  5. 父组件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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 08:56:08