修复定时器与动画回调清理不完整问题
This commit is contained in:
@@ -167,10 +167,15 @@
|
||||
- 先清理 error 级规则,再统一处理 warning;
|
||||
- 针对 hooks 规则建立最小回归测试。
|
||||
|
||||
### P2-5 定时器清理不完整(潜在内存泄漏/卸载后状态写入)
|
||||
### P2-5 定时器清理不完整(潜在内存泄漏/卸载后状态写入)【已完成】
|
||||
- 修复状态:✅ 已完成(2026-03-03)
|
||||
- 修复内容:
|
||||
- `useBlindboxIdeas` 增加统一 `useEffect` cleanup,卸载时清理所有 `timersRef`;
|
||||
- `useBlindboxDraw` 增加 confetti 停止逻辑(timeout + `requestAnimationFrame` + `confetti.reset`),并在 `handleContinue` 与 unmount 时清理;
|
||||
- 同步修正盲盒房间页与 `useBlindboxRoom` 的定时器 cleanup 方式,避免 ref 清理时机不稳定。
|
||||
- 证据:
|
||||
- `src/hooks/useBlindboxIdeas.ts:52` + `:129`(保存定时器,但无统一 cleanup)
|
||||
- `src/hooks/useBlindboxDraw.ts:25` + `:38`(定时器与动画回调未见 unmount 清理)
|
||||
- `src/hooks/useBlindboxIdeas.ts` 已新增 `timersRef` 统一清理;
|
||||
- `src/hooks/useBlindboxDraw.ts` 已新增 confetti 与动画回调销毁逻辑。
|
||||
- 影响:
|
||||
- 页面切换或快速操作下可能出现卸载后 setState、额外渲染噪音。
|
||||
- 建议:
|
||||
|
||||
@@ -42,7 +42,11 @@ export default function BlindboxRoomPage() {
|
||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => { timersRef.current.forEach(clearTimeout); };
|
||||
const timers = timersRef.current;
|
||||
return () => {
|
||||
timers.forEach(clearTimeout);
|
||||
timers.length = 0;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Hook: Room
|
||||
@@ -58,7 +62,6 @@ export default function BlindboxRoomPage() {
|
||||
input, setInput, submitting, suggestions, suggestionsLoading,
|
||||
suggestionsSource, poolCount, setPoolCount, myIdeas, drawnHistory,
|
||||
setDrawnHistory, submitFlash, error, setError, inputRef,
|
||||
boxControls: ideaBoxControls,
|
||||
fetchIdeas, fetchSuggestions, refreshSuggestions,
|
||||
handleSubmit, handleEditIdea, handleDeleteIdea,
|
||||
} = useBlindboxIdeas(room, profile);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useCallback } from "react";
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { useAnimation } from "framer-motion";
|
||||
import confetti from "canvas-confetti";
|
||||
import type { DrawnIdea } from "@/components/BlindboxDrawnHistory";
|
||||
@@ -23,8 +23,21 @@ export function useBlindboxDraw(
|
||||
const boxControls = useAnimation();
|
||||
const confettiAliveRef = useRef(false);
|
||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
const animationFrameRef = useRef<number | null>(null);
|
||||
|
||||
const stopConfetti = useCallback(() => {
|
||||
confettiAliveRef.current = false;
|
||||
if (animationFrameRef.current !== null) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
animationFrameRef.current = null;
|
||||
}
|
||||
timersRef.current.forEach(clearTimeout);
|
||||
timersRef.current = [];
|
||||
if (typeof confetti.reset === "function") confetti.reset();
|
||||
}, []);
|
||||
|
||||
const fireConfetti = useCallback(() => {
|
||||
stopConfetti();
|
||||
const colors = ["#a855f7", "#6366f1", "#ec4899", "#f59e0b", "#10b981"];
|
||||
confetti({ particleCount: 100, spread: 120, origin: { y: 0.4 }, colors, startVelocity: 45, ticks: 250 });
|
||||
confettiAliveRef.current = true;
|
||||
@@ -33,10 +46,21 @@ export function useBlindboxDraw(
|
||||
if (Date.now() > end || !confettiAliveRef.current) return;
|
||||
confetti({ particleCount: 3, angle: 60, spread: 55, origin: { x: 0, y: 0.6 }, colors, startVelocity: 35, ticks: 150 });
|
||||
confetti({ particleCount: 3, angle: 120, spread: 55, origin: { x: 1, y: 0.6 }, colors, startVelocity: 35, ticks: 150 });
|
||||
requestAnimationFrame(frame);
|
||||
animationFrameRef.current = requestAnimationFrame(frame);
|
||||
};
|
||||
timersRef.current.push(setTimeout(frame, 200));
|
||||
}, []);
|
||||
timersRef.current.push(
|
||||
setTimeout(() => {
|
||||
animationFrameRef.current = requestAnimationFrame(frame);
|
||||
}, 200),
|
||||
);
|
||||
}, [stopConfetti]);
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
stopConfetti();
|
||||
},
|
||||
[stopConfetti],
|
||||
);
|
||||
|
||||
const handleDraw = async () => {
|
||||
if (poolCount === 0 || !profile || !room) {
|
||||
@@ -78,10 +102,11 @@ export function useBlindboxDraw(
|
||||
};
|
||||
|
||||
const handleContinue = useCallback(() => {
|
||||
stopConfetti();
|
||||
setPhase("pool");
|
||||
setRevealedIdea(null);
|
||||
setShowShareCard(false);
|
||||
}, [setPhase]);
|
||||
}, [setPhase, stopConfetti]);
|
||||
|
||||
return {
|
||||
revealedIdea,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback, useRef } from "react";
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { useAnimation } from "framer-motion";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import type { MyIdea } from "@/components/BlindboxMyIdeas";
|
||||
@@ -51,6 +51,14 @@ export function useBlindboxIdeas(room: RoomInfo | null, profile: UserProfile | n
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const timers = timersRef.current;
|
||||
return () => {
|
||||
timers.forEach(clearTimeout);
|
||||
timers.length = 0;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const fetchIdeas = useCallback(async () => {
|
||||
if (!room || !profile) return;
|
||||
try {
|
||||
|
||||
@@ -37,7 +37,11 @@ export function useBlindboxRoom(code: string) {
|
||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => { timersRef.current.forEach(clearTimeout); };
|
||||
const timers = timersRef.current;
|
||||
return () => {
|
||||
timers.forEach(clearTimeout);
|
||||
timers.length = 0;
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user