Skip to content

闭包陷阱问题 - React函数组件深度解析

1. 什么是闭包陷阱

1.1 闭包基础

javascript
// JavaScript闭包示例
function createCounter() {
  let count = 0;  // 外部变量
  
  return function increment() {
    count++;  // 闭包捕获了count
    console.log(count);
  };
}

const counter1 = createCounter();
counter1();  // 1
counter1();  // 2

const counter2 = createCounter();
counter2();  // 1 (新的闭包)

1.2 React中的闭包

jsx
function Counter() {
  const [count, setCount] = useState(0);
  
  // 每次渲染都会创建新的handleClick函数
  // 这个函数捕获了当前渲染时的count值
  const handleClick = () => {
    console.log('Count:', count);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={handleClick}>Log Count</button>
    </div>
  );
}

1.3 闭包陷阱定义

typescript
const closureTrapDefinition = {
  定义: '函数捕获了过时的state或props值,导致使用旧数据的问题',
  常见场景: [
    'useEffect中的定时器/事件监听',
    '异步回调函数',
    'useCallback缓存的函数',
    'ref回调',
    '第三方库的回调'
  ],
  后果: [
    '显示错误数据',
    '状态更新不符合预期',
    '内存泄漏',
    '难以调试'
  ]
};

2. 典型闭包陷阱场景

2.1 定时器中的闭包

jsx
// ❌ 问题: 定时器捕获了初始count
function BadTimer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // 这里的count永远是0!
      console.log('Current count:', count);
      setCount(count + 1);  // 总是0 + 1
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);  // 空依赖,effect只执行一次
  
  return <div>Count: {count}</div>;
}

// ✅ 解决方案1: 函数式更新
function TimerSolution1() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // 使用函数式更新,获取最新state
      setCount(prevCount => {
        console.log('Current count:', prevCount);
        return prevCount + 1;
      });
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return <div>Count: {count}</div>;
}

// ✅ 解决方案2: useRef保存最新值
function TimerSolution2() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  
  // 每次渲染更新ref
  useEffect(() => {
    countRef.current = count;
  }, [count]);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // 通过ref读取最新值
      console.log('Current count:', countRef.current);
      setCount(countRef.current + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return <div>Count: {count}</div>;
}

// ✅ 解决方案3: 自定义Hook
function useInterval(callback, delay) {
  const savedCallback = useRef(callback);
  
  // 保存最新回调
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);
  
  // 设置定时器
  useEffect(() => {
    if (delay === null) return;
    
    const timer = setInterval(() => {
      savedCallback.current();
    }, delay);
    
    return () => clearInterval(timer);
  }, [delay]);
}

function TimerSolution3() {
  const [count, setCount] = useState(0);
  
  useInterval(() => {
    console.log('Current count:', count);
    setCount(count + 1);
  }, 1000);
  
  return <div>Count: {count}</div>;
}

2.2 事件监听器中的闭包

jsx
// ❌ 问题: 事件监听器捕获旧值
function BadEventListener() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const handleKeyPress = (e) => {
      if (e.key === 'Enter') {
        // 这里的count是添加监听器时的值
        console.log('Count:', count);
      }
    };
    
    window.addEventListener('keypress', handleKeyPress);
    
    return () => {
      window.removeEventListener('keypress', handleKeyPress);
    };
  }, []);  // 空依赖
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Press Enter to log count</p>
    </div>
  );
}

// ✅ 解决方案1: 添加count依赖
function EventListenerSolution1() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const handleKeyPress = (e) => {
      if (e.key === 'Enter') {
        console.log('Count:', count);
      }
    };
    
    window.addEventListener('keypress', handleKeyPress);
    
    return () => {
      window.removeEventListener('keypress', handleKeyPress);
    };
  }, [count]);  // 添加count依赖
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Press Enter to log count</p>
    </div>
  );
}

// ✅ 解决方案2: useRef
function EventListenerSolution2() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);
  
  useEffect(() => {
    countRef.current = count;
  }, [count]);
  
  useEffect(() => {
    const handleKeyPress = (e) => {
      if (e.key === 'Enter') {
        console.log('Count:', countRef.current);
      }
    };
    
    window.addEventListener('keypress', handleKeyPress);
    
    return () => {
      window.removeEventListener('keypress', handleKeyPress);
    };
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Press Enter to log count</p>
    </div>
  );
}

// ✅ 解决方案3: 自定义useEventListener Hook
function useEventListener(event, handler, element = window) {
  const savedHandler = useRef(handler);
  
  useEffect(() => {
    savedHandler.current = handler;
  }, [handler]);
  
  useEffect(() => {
    const eventListener = (e) => savedHandler.current(e);
    element.addEventListener(event, eventListener);
    
    return () => {
      element.removeEventListener(event, eventListener);
    };
  }, [event, element]);
}

function EventListenerSolution3() {
  const [count, setCount] = useState(0);
  
  useEventListener('keypress', (e) => {
    if (e.key === 'Enter') {
      console.log('Count:', count);
    }
  });
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <p>Press Enter to log count</p>
    </div>
  );
}

2.3 异步操作中的闭包

jsx
// ❌ 问题: 异步回调捕获旧值
function BadAsync() {
  const [userId, setUserId] = useState(1);
  const [user, setUser] = useState(null);
  
  const fetchUser = async () => {
    // 这里的userId可能已经过时
    await delay(1000);
    const data = await getUserById(userId);
    setUser(data);
  };
  
  return (
    <div>
      <button onClick={fetchUser}>Fetch User</button>
      <button onClick={() => setUserId(userId + 1)}>Next User</button>
      <div>{user?.name}</div>
    </div>
  );
}

// ✅ 解决方案1: useEffect
function AsyncSolution1() {
  const [userId, setUserId] = useState(1);
  const [user, setUser] = useState(null);
  
  useEffect(() => {
    let cancelled = false;
    
    const fetchUser = async () => {
      const data = await getUserById(userId);
      if (!cancelled) {
        setUser(data);
      }
    };
    
    fetchUser();
    
    return () => {
      cancelled = true;
    };
  }, [userId]);  // userId变化时重新获取
  
  return (
    <div>
      <button onClick={() => setUserId(userId + 1)}>Next User</button>
      <div>{user?.name}</div>
    </div>
  );
}

// ✅ 解决方案2: useRef
function AsyncSolution2() {
  const [userId, setUserId] = useState(1);
  const [user, setUser] = useState(null);
  const userIdRef = useRef(userId);
  
  useEffect(() => {
    userIdRef.current = userId;
  }, [userId]);
  
  const fetchUser = async () => {
    // 使用ref获取最新值
    const id = userIdRef.current;
    const data = await getUserById(id);
    setUser(data);
  };
  
  return (
    <div>
      <button onClick={fetchUser}>Fetch User</button>
      <button onClick={() => setUserId(userId + 1)}>Next User</button>
      <div>{user?.name}</div>
    </div>
  );
}

// ✅ 解决方案3: 使用useCallback
function AsyncSolution3() {
  const [userId, setUserId] = useState(1);
  const [user, setUser] = useState(null);
  
  const fetchUser = useCallback(async () => {
    const data = await getUserById(userId);
    setUser(data);
  }, [userId]);  // userId变化时更新函数
  
  return (
    <div>
      <button onClick={fetchUser}>Fetch User</button>
      <button onClick={() => setUserId(userId + 1)}>Next User</button>
      <div>{user?.name}</div>
    </div>
  );
}

2.4 useCallback中的闭包

jsx
// ❌ 问题: useCallback捕获旧值
function BadCallback() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');
  
  // 因为依赖数组为空,handleSubmit永远捕获count=0
  const handleSubmit = useCallback(() => {
    console.log('Submitting with count:', count);
    setMessage(`Submitted with count: ${count}`);
  }, []);  // 空依赖
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={handleSubmit}>Submit</button>
      <p>{message}</p>
    </div>
  );
}

// ✅ 解决方案1: 添加依赖
function CallbackSolution1() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');
  
  const handleSubmit = useCallback(() => {
    console.log('Submitting with count:', count);
    setMessage(`Submitted with count: ${count}`);
  }, [count]);  // 添加count依赖
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={handleSubmit}>Submit</button>
      <p>{message}</p>
    </div>
  );
}

// ✅ 解决方案2: useRef
function CallbackSolution2() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');
  const countRef = useRef(count);
  
  useEffect(() => {
    countRef.current = count;
  }, [count]);
  
  const handleSubmit = useCallback(() => {
    // 使用ref读取最新值
    console.log('Submitting with count:', countRef.current);
    setMessage(`Submitted with count: ${countRef.current}`);
  }, []);  // 空依赖,但能读取最新count
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={handleSubmit}>Submit</button>
      <p>{message}</p>
    </div>
  );
}

// ✅ 解决方案3: 函数式更新
function CallbackSolution3() {
  const [count, setCount] = useState(0);
  const [message, setMessage] = useState('');
  
  const handleSubmit = useCallback(() => {
    // 使用函数式更新获取最新count
    setMessage(prevMessage => {
      setCount(c => {
        console.log('Submitting with count:', c);
        return c;
      });
      return `Submitted`;
    });
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={handleSubmit}>Submit</button>
      <p>{message}</p>
    </div>
  );
}

3. 复杂闭包场景

3.1 多层嵌套闭包

jsx
// ❌ 问题: 多层闭包捕获
function BadNestedClosure() {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(2);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // 第一层闭包
      setTimeout(() => {
        // 第二层闭包,捕获了初始的count和multiplier
        console.log('Result:', count * multiplier);
      }, 500);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Multiplier: {multiplier}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setMultiplier(multiplier + 1)}>Increment Multiplier</button>
    </div>
  );
}

// ✅ 解决方案: 使用ref保存所有需要的值
function NestedClosureSolution() {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(2);
  
  const countRef = useRef(count);
  const multiplierRef = useRef(multiplier);
  
  useEffect(() => {
    countRef.current = count;
    multiplierRef.current = multiplier;
  }, [count, multiplier]);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setTimeout(() => {
        console.log('Result:', countRef.current * multiplierRef.current);
      }, 500);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Multiplier: {multiplier}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <button onClick={() => setMultiplier(multiplier + 1)}>Increment Multiplier</button>
    </div>
  );
}

3.2 闭包与Context

jsx
const ThemeContext = createContext('light');

// ❌ 问题: 捕获了旧的context值
function BadContextClosure() {
  const theme = useContext(ThemeContext);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // theme是添加effect时的值
      console.log('Current theme:', theme);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);  // 空依赖
  
  return <div>Theme: {theme}</div>;
}

// ✅ 解决方案1: 添加依赖
function ContextClosureSolution1() {
  const theme = useContext(ThemeContext);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Current theme:', theme);
    }, 1000);
    
    return () => clearInterval(timer);
  }, [theme]);  // 添加theme依赖
  
  return <div>Theme: {theme}</div>;
}

// ✅ 解决方案2: useRef
function ContextClosureSolution2() {
  const theme = useContext(ThemeContext);
  const themeRef = useRef(theme);
  
  useEffect(() => {
    themeRef.current = theme;
  }, [theme]);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Current theme:', themeRef.current);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return <div>Theme: {theme}</div>;
}

3.3 闭包与自定义Hook

jsx
// ❌ 问题: 自定义Hook中的闭包陷阱
function useBadFetch(url) {
  const [data, setData] = useState(null);
  
  const refetch = useCallback(() => {
    // url是创建refetch时的值
    fetch(url)
      .then(res => res.json())
      .then(setData);
  }, []);  // 空依赖
  
  useEffect(() => {
    refetch();
  }, [url, refetch]);
  
  return { data, refetch };
}

// ✅ 解决方案1: 添加url依赖
function useGoodFetch1(url) {
  const [data, setData] = useState(null);
  
  const refetch = useCallback(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData);
  }, [url]);  // 添加url依赖
  
  useEffect(() => {
    refetch();
  }, [refetch]);
  
  return { data, refetch };
}

// ✅ 解决方案2: 分离逻辑
function useGoodFetch2(url) {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData);
  }, [url]);
  
  const refetch = useCallback(() => {
    fetch(url)
      .then(res => res.json())
      .then(setData);
  }, [url]);
  
  return { data, refetch };
}

4. 闭包陷阱检测

4.1 ESLint检测

javascript
// .eslintrc.js
module.exports = {
  extends: ['react-app'],
  plugins: ['react-hooks'],
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn'  // 检测依赖项
  }
};

4.2 自定义检测Hook

jsx
// 检测stale closure
function useStaleClosureDetector(name, value) {
  const valueRef = useRef(value);
  const renderCount = useRef(0);
  
  useEffect(() => {
    renderCount.current++;
    
    if (valueRef.current !== value) {
      console.warn(
        `[Stale Closure] ${name} changed from`,
        valueRef.current,
        'to',
        value,
        'at render',
        renderCount.current
      );
    }
    
    valueRef.current = value;
  });
}

// 使用
function Component() {
  const [count, setCount] = useState(0);
  
  useStaleClosureDetector('count', count);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Count:', count);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);  // 故意遗漏count,触发警告
  
  return <div>Count: {count}</div>;
}

4.3 运行时检测

jsx
// 创建带检测的useCallback
function useCallbackWithCheck(callback, deps, debugName) {
  const prevDepsRef = useRef(deps);
  
  useEffect(() => {
    if (prevDepsRef.current) {
      const changed = deps.some((dep, i) => dep !== prevDepsRef.current[i]);
      if (changed) {
        console.log(`[useCallback] ${debugName} dependencies changed:`, {
          prev: prevDepsRef.current,
          current: deps
        });
      }
    }
    prevDepsRef.current = deps;
  });
  
  return useCallback(callback, deps);
}

// 使用
function Component() {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallbackWithCheck(() => {
    console.log('Count:', count);
  }, [count], 'handleClick');
  
  return <button onClick={handleClick}>Log Count</button>;
}

5. 解决方案模式

5.1 useRef模式

jsx
// 通用的useLatest Hook
function useLatest(value) {
  const ref = useRef(value);
  
  useEffect(() => {
    ref.current = value;
  }, [value]);
  
  return ref;
}

// 使用
function Component() {
  const [count, setCount] = useState(0);
  const countRef = useLatest(count);
  
  useEffect(() => {
    const timer = setInterval(() => {
      console.log('Latest count:', countRef.current);
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return <div>Count: {count}</div>;
}

5.2 函数式更新模式

jsx
// 通用的状态更新Hook
function useStateWithUpdater(initialState) {
  const [state, setState] = useState(initialState);
  
  const updateState = useCallback((updater) => {
    setState(prev => {
      const next = typeof updater === 'function' ? updater(prev) : updater;
      return next;
    });
  }, []);
  
  return [state, updateState];
}

// 使用
function Component() {
  const [count, setCount] = useStateWithUpdater(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      // 始终获取最新state
      setCount(c => c + 1);
    }, 1000);
    
    return () => clearInterval(timer);
  }, [setCount]);
  
  return <div>Count: {count}</div>;
}

5.3 事件发射器模式

jsx
// 创建事件发射器
class EventEmitter {
  constructor() {
    this.events = {};
  }
  
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
    
    return () => {
      this.events[event] = this.events[event].filter(l => l !== listener);
    };
  }
  
  emit(event, ...args) {
    if (this.events[event]) {
      this.events[event].forEach(listener => listener(...args));
    }
  }
}

// 使用事件发射器避免闭包
const emitter = new EventEmitter();

function Producer() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const timer = setInterval(() => {
      setCount(c => {
        emitter.emit('count', c + 1);
        return c + 1;
      });
    }, 1000);
    
    return () => clearInterval(timer);
  }, []);
  
  return <div>Count: {count}</div>;
}

function Consumer() {
  const [receivedCount, setReceivedCount] = useState(0);
  
  useEffect(() => {
    const unsubscribe = emitter.on('count', (count) => {
      setReceivedCount(count);
    });
    
    return unsubscribe;
  }, []);
  
  return <div>Received: {receivedCount}</div>;
}

6. 高级技巧

6.1 创建稳定的回调

jsx
// useEvent Hook (React RFC)
function useEvent(handler) {
  const handlerRef = useRef(null);
  
  // 始终保持最新handler
  useLayoutEffect(() => {
    handlerRef.current = handler;
  });
  
  // 返回稳定的包装函数
  return useCallback((...args) => {
    const fn = handlerRef.current;
    return fn(...args);
  }, []);
}

// 使用
function Component() {
  const [count, setCount] = useState(0);
  
  // handleClick引用始终稳定,但内部逻辑始终最新
  const handleClick = useEvent(() => {
    console.log('Current count:', count);
  });
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <ExpensiveChild onClick={handleClick} />
    </div>
  );
}

const ExpensiveChild = React.memo(({ onClick }) => {
  console.log('ExpensiveChild rendered');
  return <button onClick={onClick}>Click Me</button>;
});

6.2 闭包调试器

jsx
// 创建闭包调试工具
function createClosureDebugger() {
  const closures = new Map();
  let closureId = 0;
  
  return {
    track(fn, context, name = 'anonymous') {
      const id = closureId++;
      closures.set(id, {
        name,
        context: { ...context },
        created: Date.now()
      });
      
      return (...args) => {
        const closure = closures.get(id);
        console.log(`[Closure ${id}] ${name} called:`, {
          capturedContext: closure.context,
          currentArgs: args,
          age: Date.now() - closure.created
        });
        return fn(...args);
      };
    }
  };
}

// 使用
const debugger = createClosureDebugger();

function Component() {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    const handler = debugger.track(
      () => {
        console.log('Count:', count);
      },
      { count },
      'timer-handler'
    );
    
    const timer = setInterval(handler, 1000);
    
    return () => clearInterval(timer);
  }, [count]);
  
  return <div>Count: {count}</div>;
}

6.3 自动修复闭包

jsx
// 自动包装闭包以使用最新值
function useAutoFix(callback) {
  const callbackRef = useRef(callback);
  
  useEffect(() => {
    callbackRef.current = callback;
  });
  
  return useCallback((...args) => {
    return callbackRef.current(...args);
  }, []);
}

// 使用
function Component() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState('');
  
  // 自动修复的回调
  const handleSubmit = useAutoFix(() => {
    console.log('Submitting:', { count, text });
  });
  
  useEffect(() => {
    // 即使依赖为空,handleSubmit也能访问最新值
    const timer = setInterval(() => {
      handleSubmit();
    }, 2000);
    
    return () => clearInterval(timer);
  }, [handleSubmit]);
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <input value={text} onChange={e => setText(e.target.value)} />
    </div>
  );
}

7. 实战案例

7.1 WebSocket连接管理

jsx
function useWebSocket(url) {
  const [messages, setMessages] = useState([]);
  const [connected, setConnected] = useState(false);
  
  // 保存最新的消息处理器
  const handlersRef = useRef({
    onMessage: null,
    onError: null
  });
  
  const connect = useCallback(() => {
    const ws = new WebSocket(url);
    
    ws.onopen = () => {
      setConnected(true);
    };
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      
      // 使用函数式更新避免闭包
      setMessages(prev => [...prev, message]);
      
      // 调用最新的处理器
      if (handlersRef.current.onMessage) {
        handlersRef.current.onMessage(message);
      }
    };
    
    ws.onerror = (error) => {
      if (handlersRef.current.onError) {
        handlersRef.current.onError(error);
      }
    };
    
    ws.onclose = () => {
      setConnected(false);
    };
    
    return ws;
  }, [url]);
  
  const setMessageHandler = useCallback((handler) => {
    handlersRef.current.onMessage = handler;
  }, []);
  
  const setErrorHandler = useCallback((handler) => {
    handlersRef.current.onError = handler;
  }, []);
  
  useEffect(() => {
    const ws = connect();
    
    return () => {
      ws.close();
    };
  }, [connect]);
  
  return {
    messages,
    connected,
    setMessageHandler,
    setErrorHandler
  };
}

// 使用
function ChatRoom() {
  const [currentUser, setCurrentUser] = useState('Alice');
  const { messages, connected, setMessageHandler } = useWebSocket('ws://chat.server');
  
  // 动态设置消息处理器
  useEffect(() => {
    setMessageHandler((message) => {
      // 这里能访问最新的currentUser
      console.log(`${currentUser} received:`, message);
    });
  }, [currentUser, setMessageHandler]);
  
  return (
    <div>
      <p>Connected: {connected ? 'Yes' : 'No'}</p>
      <p>Current User: {currentUser}</p>
      <button onClick={() => setCurrentUser('Bob')}>Switch to Bob</button>
      <ul>
        {messages.map((msg, i) => (
          <li key={i}>{msg.text}</li>
        ))}
      </ul>
    </div>
  );
}

7.2 表单处理

jsx
function useForm(initialValues, onSubmit) {
  const [values, setValues] = useState(initialValues);
  const [errors, setErrors] = useState({});
  
  // 保存最新的验证规则和提交处理器
  const latestRef = useRef({ onSubmit });
  
  useEffect(() => {
    latestRef.current.onSubmit = onSubmit;
  }, [onSubmit]);
  
  const handleChange = useCallback((name, value) => {
    setValues(prev => ({
      ...prev,
      [name]: value
    }));
  }, []);
  
  const handleSubmit = useCallback((e) => {
    e.preventDefault();
    
    // 使用最新的onSubmit
    if (latestRef.current.onSubmit) {
      latestRef.current.onSubmit(values);
    }
  }, [values]);
  
  return {
    values,
    errors,
    handleChange,
    handleSubmit
  };
}

// 使用
function LoginForm() {
  const [user, setUser] = useState(null);
  
  const { values, handleChange, handleSubmit } = useForm(
    { username: '', password: '' },
    (formValues) => {
      // 这里能访问最新的user
      console.log('Current user:', user);
      console.log('Form values:', formValues);
    }
  );
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={values.username}
        onChange={e => handleChange('username', e.target.value)}
      />
      <input
        type="password"
        value={values.password}
        onChange={e => handleChange('password', e.target.value)}
      />
      <button type="submit">Login</button>
      <button onClick={() => setUser({ name: 'Test' })}>Set Test User</button>
    </form>
  );
}

8. 最佳实践

8.1 原则总结

typescript
const closureBestPractices = [
  '1. 始终完整声明useEffect/useCallback的依赖',
  '2. 对于不能添加依赖的情况,使用useRef',
  '3. 优先使用函数式更新',
  '4. 创建自定义Hook封装常见模式',
  '5. 使用ESLint检测依赖问题',
  '6. 理解每次渲染都会创建新的闭包',
  '7. 避免在闭包中读取复杂对象',
  '8. 使用useEvent模式创建稳定回调',
  '9. 警惕定时器和事件监听器',
  '10. 异步操作要考虑组件卸载'
];

8.2 检查清单

typescript
const closureChecklist = {
  定时器: [
    '✅ 是否使用了函数式更新?',
    '✅ 是否需要useRef?',
    '✅ 清理函数是否正确?'
  ],
  事件监听: [
    '✅ 依赖数组是否包含所有变量?',
    '✅ 是否考虑使用useEventListener?',
    '✅ 是否正确移除监听器?'
  ],
  异步操作: [
    '✅ 是否处理了组件卸载?',
    '✅ 是否有竞态条件?',
    '✅ 是否使用了最新的props/state?'
  ],
  回调函数: [
    '✅ useCallback依赖是否完整?',
    '✅ 是否需要useEvent模式?',
    '✅ 子组件是否需要memo?'
  ]
};

9. 总结

闭包陷阱是React函数组件中最常见的问题之一,理解其原理和解决方案至关重要:

  1. 根本原因: 函数捕获了创建时的变量值
  2. 常见场景: 定时器、事件监听、异步回调、useCallback
  3. 检测方法: ESLint、自定义检测Hook、运行时调试
  4. 解决方案: useRef、函数式更新、正确的依赖数组、useEvent模式
  5. 最佳实践: 理解闭包、善用工具、封装常见模式

掌握这些知识可以避免闭包相关的bug,写出更可靠的React应用。