修复定时器与动画回调清理不完整问题
This commit is contained in:
@@ -167,10 +167,15 @@
|
|||||||
- 先清理 error 级规则,再统一处理 warning;
|
- 先清理 error 级规则,再统一处理 warning;
|
||||||
- 针对 hooks 规则建立最小回归测试。
|
- 针对 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/useBlindboxIdeas.ts` 已新增 `timersRef` 统一清理;
|
||||||
- `src/hooks/useBlindboxDraw.ts:25` + `:38`(定时器与动画回调未见 unmount 清理)
|
- `src/hooks/useBlindboxDraw.ts` 已新增 confetti 与动画回调销毁逻辑。
|
||||||
- 影响:
|
- 影响:
|
||||||
- 页面切换或快速操作下可能出现卸载后 setState、额外渲染噪音。
|
- 页面切换或快速操作下可能出现卸载后 setState、额外渲染噪音。
|
||||||
- 建议:
|
- 建议:
|
||||||
|
|||||||
@@ -42,7 +42,11 @@ export default function BlindboxRoomPage() {
|
|||||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => { timersRef.current.forEach(clearTimeout); };
|
const timers = timersRef.current;
|
||||||
|
return () => {
|
||||||
|
timers.forEach(clearTimeout);
|
||||||
|
timers.length = 0;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Hook: Room
|
// Hook: Room
|
||||||
@@ -58,7 +62,6 @@ export default function BlindboxRoomPage() {
|
|||||||
input, setInput, submitting, suggestions, suggestionsLoading,
|
input, setInput, submitting, suggestions, suggestionsLoading,
|
||||||
suggestionsSource, poolCount, setPoolCount, myIdeas, drawnHistory,
|
suggestionsSource, poolCount, setPoolCount, myIdeas, drawnHistory,
|
||||||
setDrawnHistory, submitFlash, error, setError, inputRef,
|
setDrawnHistory, submitFlash, error, setError, inputRef,
|
||||||
boxControls: ideaBoxControls,
|
|
||||||
fetchIdeas, fetchSuggestions, refreshSuggestions,
|
fetchIdeas, fetchSuggestions, refreshSuggestions,
|
||||||
handleSubmit, handleEditIdea, handleDeleteIdea,
|
handleSubmit, handleEditIdea, handleDeleteIdea,
|
||||||
} = useBlindboxIdeas(room, profile);
|
} = useBlindboxIdeas(room, profile);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useCallback } from "react";
|
import { useState, useRef, useCallback, useEffect } from "react";
|
||||||
import { useAnimation } from "framer-motion";
|
import { useAnimation } from "framer-motion";
|
||||||
import confetti from "canvas-confetti";
|
import confetti from "canvas-confetti";
|
||||||
import type { DrawnIdea } from "@/components/BlindboxDrawnHistory";
|
import type { DrawnIdea } from "@/components/BlindboxDrawnHistory";
|
||||||
@@ -23,8 +23,21 @@ export function useBlindboxDraw(
|
|||||||
const boxControls = useAnimation();
|
const boxControls = useAnimation();
|
||||||
const confettiAliveRef = useRef(false);
|
const confettiAliveRef = useRef(false);
|
||||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
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(() => {
|
const fireConfetti = useCallback(() => {
|
||||||
|
stopConfetti();
|
||||||
const colors = ["#a855f7", "#6366f1", "#ec4899", "#f59e0b", "#10b981"];
|
const colors = ["#a855f7", "#6366f1", "#ec4899", "#f59e0b", "#10b981"];
|
||||||
confetti({ particleCount: 100, spread: 120, origin: { y: 0.4 }, colors, startVelocity: 45, ticks: 250 });
|
confetti({ particleCount: 100, spread: 120, origin: { y: 0.4 }, colors, startVelocity: 45, ticks: 250 });
|
||||||
confettiAliveRef.current = true;
|
confettiAliveRef.current = true;
|
||||||
@@ -33,10 +46,21 @@ export function useBlindboxDraw(
|
|||||||
if (Date.now() > end || !confettiAliveRef.current) return;
|
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: 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 });
|
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 () => {
|
const handleDraw = async () => {
|
||||||
if (poolCount === 0 || !profile || !room) {
|
if (poolCount === 0 || !profile || !room) {
|
||||||
@@ -78,10 +102,11 @@ export function useBlindboxDraw(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleContinue = useCallback(() => {
|
const handleContinue = useCallback(() => {
|
||||||
|
stopConfetti();
|
||||||
setPhase("pool");
|
setPhase("pool");
|
||||||
setRevealedIdea(null);
|
setRevealedIdea(null);
|
||||||
setShowShareCard(false);
|
setShowShareCard(false);
|
||||||
}, [setPhase]);
|
}, [setPhase, stopConfetti]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
revealedIdea,
|
revealedIdea,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useRef } from "react";
|
import { useState, useCallback, useEffect, useRef } from "react";
|
||||||
import { useAnimation } from "framer-motion";
|
import { useAnimation } from "framer-motion";
|
||||||
import { useToast } from "@/hooks/useToast";
|
import { useToast } from "@/hooks/useToast";
|
||||||
import type { MyIdea } from "@/components/BlindboxMyIdeas";
|
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 inputRef = useRef<HTMLInputElement>(null);
|
||||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timers = timersRef.current;
|
||||||
|
return () => {
|
||||||
|
timers.forEach(clearTimeout);
|
||||||
|
timers.length = 0;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const fetchIdeas = useCallback(async () => {
|
const fetchIdeas = useCallback(async () => {
|
||||||
if (!room || !profile) return;
|
if (!room || !profile) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ export function useBlindboxRoom(code: string) {
|
|||||||
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => { timersRef.current.forEach(clearTimeout); };
|
const timers = timersRef.current;
|
||||||
|
return () => {
|
||||||
|
timers.forEach(clearTimeout);
|
||||||
|
timers.length = 0;
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user