From 4cd593bc30c5d54b1a8e00b717d1926518fcfdce Mon Sep 17 00:00:00 2001 From: kurihada Date: Tue, 3 Mar 2026 12:27:34 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=9A=E6=97=B6=E5=99=A8?= =?UTF-8?q?=E4=B8=8E=E5=8A=A8=E7=94=BB=E5=9B=9E=E8=B0=83=E6=B8=85=E7=90=86?= =?UTF-8?q?=E4=B8=8D=E5=AE=8C=E6=95=B4=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_AUDIT_2026-03-03.md | 11 +++++++--- src/app/blindbox/[code]/page.tsx | 7 +++++-- src/hooks/useBlindboxDraw.ts | 35 +++++++++++++++++++++++++++----- src/hooks/useBlindboxIdeas.ts | 10 ++++++++- src/hooks/useBlindboxRoom.ts | 6 +++++- 5 files changed, 57 insertions(+), 12 deletions(-) diff --git a/PROJECT_AUDIT_2026-03-03.md b/PROJECT_AUDIT_2026-03-03.md index dd58b3e..21190ff 100644 --- a/PROJECT_AUDIT_2026-03-03.md +++ b/PROJECT_AUDIT_2026-03-03.md @@ -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、额外渲染噪音。 - 建议: diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index 3c58409..c6d913f 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -42,7 +42,11 @@ export default function BlindboxRoomPage() { const timersRef = useRef[]>([]); 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); diff --git a/src/hooks/useBlindboxDraw.ts b/src/hooks/useBlindboxDraw.ts index 0a17050..7fc8175 100644 --- a/src/hooks/useBlindboxDraw.ts +++ b/src/hooks/useBlindboxDraw.ts @@ -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[]>([]); + const animationFrameRef = useRef(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, diff --git a/src/hooks/useBlindboxIdeas.ts b/src/hooks/useBlindboxIdeas.ts index 47e3ed6..ef7e017 100644 --- a/src/hooks/useBlindboxIdeas.ts +++ b/src/hooks/useBlindboxIdeas.ts @@ -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(null); const timersRef = useRef[]>([]); + useEffect(() => { + const timers = timersRef.current; + return () => { + timers.forEach(clearTimeout); + timers.length = 0; + }; + }, []); + const fetchIdeas = useCallback(async () => { if (!room || !profile) return; try { diff --git a/src/hooks/useBlindboxRoom.ts b/src/hooks/useBlindboxRoom.ts index 8080e97..11f3766 100644 --- a/src/hooks/useBlindboxRoom.ts +++ b/src/hooks/useBlindboxRoom.ts @@ -37,7 +37,11 @@ export function useBlindboxRoom(code: string) { const timersRef = useRef[]>([]); useEffect(() => { - return () => { timersRef.current.forEach(clearTimeout); }; + const timers = timersRef.current; + return () => { + timers.forEach(clearTimeout); + timers.length = 0; + }; }, []); useEffect(() => {