背景
翻阅 useMount
和 useUnmount 的源码我们会有一个疑惑:为什么 useUnmount
要用 useRef(useLatest
底层使用的是 useRef
),而 useMount
无需。
tsx
import { useEffect } from 'react';
import { isFunction } from '../utils';
import isDev from '../utils/isDev';
const useMount = (fn: () => void) => {
if (isDev) {
if (!isFunction(fn)) {
console.error(
`useMount: parameter \`fn\` expected to be a function, but got "${typeof fn}".`,
);
}
}
useEffect(() => {
fn?.();
}, []);
};
export default useMount;
简写为:
tsx
import { useEffect } from 'react';
const useMount = (fn: () => void) => {
useEffect(() => {
fn();
}, []);
};
export default useMount;
而 useUnmount
就难以理解些,需要 useLatest:
tsx
import { useEffect } from 'react';
import useLatest from '../useLatest';
import { isFunction } from '../utils';
import isDev from '../utils/isDev';
const useUnmount = (fn: () => void) => {
if (isDev) {
if (!isFunction(fn)) {
console.error(`useUnmount expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useLatest(fn);
useEffect(
() => () => {
fnRef.current();
},
[],
);
};
export default useUnmount;
tsx
import { useRef } from 'react';
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
export default useLatest;
简写为:
tsx
import { useEffect, useRef } from 'react';
const useUnmount = (fn: () => any): void => {
const fnRef = useRef(fn);
fnRef.current = fn;
useEffect(() => () => fnRef.current(), []);
};
我们看看业界其他流行的 hooks 库 react-use useMount
tsx
import useEffectOnce from './useEffectOnce';
const useMount = (fn: () => void) => {
useEffectOnce(() => {
fn();
});
};
export default useMount;
---
// react-use/blob/master/src/useEffectOnce.ts
import { EffectCallback, useEffect } from 'react';
const useEffectOnce = (effect: EffectCallback) => {
useEffect(effect, []);
};
export default useEffectOnce;
tsx
import { useRef } from 'react';
import useEffectOnce from './useEffectOnce';
const useUnmount = (fn: () => any): void => {
const fnRef = useRef(fn);
// update the ref each render so if it change the newest callback will be invoked
fnRef.current = fn;
useEffectOnce(() => () => fnRef.current());
};
export default useUnmount;
发现相同规律 useUnmount
必须使用 ref。且特意注释使用原因:update the ref each render so if it change the newest callback will be invoked
。
难道不用不行吗?
tsx
import { useEffect } from 'react';
const useUnmount = (fn: () => any): void =>
useEffect(() => {
return () => {
fn()
}
}, []);
};
解疑 1:为什么 useUnmount 为何需要 useRef?✔️
在实际工程项目中,卸载时需要最新回调函数的情况非常常见,这确实有实质性的意义。回调函数的变化往往与 组件状态/Props 变化 、闭包问题 或 动态行为调整 相关。下面通过具体场景说明:
1. 回调函数依赖组件状态或 Props
这是最常见的场景。如果卸载时的回调函数依赖了组件的最新状态或 Props,但未正确更新,就会导致闭包问题(stale closure)。
示例:记录卸载时的数据
tsx
function UserProfile({ userId }) {
const [data, setData] = useState(null);
// 回调函数依赖了最新的 `userId` 和 `data`
const logUnmount = () => {
console.log(`User ${userId} unmounted with data:`, data);
};
useUnmount(logUnmount); // 必须确保卸载时调用的是最新的 logUnmount
// ... 数据加载逻辑 ...
}
- 如果
userId
或data
变化,但卸载时调用的回调是旧的,就会记录错误的信息。 - 解决方法 :用
useRef
保持最新回调。
2. 动态行为调整
某些情况下,组件的卸载行为需要根据运行时条件动态调整。
示例:根据权限清理资源
tsx
function DocumentEditor() {
const [userRole, setUserRole] = useState('viewer');
// 卸载时的清理逻辑可能因用户角色而异
const cleanup = () => {
if (userRole === 'admin') {
// 管理员需要额外清理敏感数据
clearSensitiveData();
}
saveDraft(); // 普通用户只保存草稿
};
useUnmount(cleanup); // 必须用最新的 cleanup
}
- 如果用户权限从
viewer
变为admin
,但卸载时调用了旧的cleanup
,就会漏掉敏感数据清理。
3. 避免内存泄漏
如果回调函数引用了外部变量(如 API 订阅、DOM 事件),但未更新到最新版本,可能导致资源未正确释放。
示例:取消订阅事件
tsx
function ChatRoom() {
const [roomId, setRoomId] = useState('general');
const handleUnmount = () => {
// 必须根据最新的 roomId 取消订阅
unsubscribe(roomId);
};
useUnmount(handleUnmount);
// 如果 roomId 变化但卸载时用了旧回调,会导致订阅泄漏!
}
若不用 ref
tsx
useEffect(() => () => fn(), []); // 问题:fn 可能过时
- 如果
fn
依赖了外部变量(如state/props
),而依赖项数组为空,卸载时调用的会是 初始渲染时的旧fn
,引发闭包问题。 - React 官方建议 :如果 Hook 接受回调函数作为参数,应该确保回调是最新的(参考 useEvent RFC 的讨论)。
实际工程中的解决方案
- 推荐模式 :使用
useRef
保存最新回调(原始代码的实现)。 - 工具库方案 :像 ahooks 的
useUnmount
、react-use 的useUnmount
均采用此设计。 - 未来方案 :React 可能推出
useEvent
(暂未正式发布 rfcs-0000-useevent.md),专门解决此类问题。目前可以使用 react-use www.reactuse.com/effect/usee... 或 ahooks useLatest,或者干脆自行封装,
在工程实践中,除非能绝对保证回调函数不会变化 ,否则使用 useRef
保存最新值是更安全的选择。
解疑 2:为什么 useMount 无需 useRef?🚫
二者执行时机不同,渲染完毕执行,此时记录的是渲染之前的值,记录完毕立马执行,不存在过时的说法。
而 useUnmount 实在渲染完毕后执行,不能仅仅记住渲染那一刻的"快照"而必须是"Latest"的。