Appearance
useMemo和useCallback手写实现 - 性能优化Hook深度解析
1. useMemo和useCallback概述
1.1 核心概念
typescript
const hooksConcept = {
useMemo: {
作用: '缓存计算结果',
用法: 'const value = useMemo(() => compute(a, b), [a, b])',
返回: '计算结果',
场景: ['昂贵计算', '引用稳定']
},
useCallback: {
作用: '缓存函数引用',
用法: 'const callback = useCallback(() => {}, [deps])',
返回: '函数引用',
场景: ['传递给子组件', '依赖数组中的函数']
},
关系: 'useCallback(fn, deps) = useMemo(() => fn, deps)',
性能优化原理: {
问题: '每次渲染创建新对象/函数',
导致: '子组件不必要的重渲染',
解决: '缓存引用,保持稳定性'
}
};1.2 数据结构
typescript
// Hook节点(复用useState的结构)
interface Hook {
memoizedState: any; // [value, deps]
baseState: null;
baseQueue: null;
queue: null;
next: Hook | null;
}
// useMemo/useCallback存储格式
type MemoState = [any, Array<any> | null];
// value: 缓存的值或函数
// deps: 依赖数组2. useMemo实现
2.1 mount阶段
typescript
/**
* mount阶段的useMemo
*/
function mountMemo<T>(
nextCreate: () => T,
deps: Array<any> | void | null
): T {
// 创建Hook节点
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 执行计算函数
const nextValue = nextCreate();
// 保存值和依赖
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
// 使用示例
const useMemoExample = `
function Component({ a, b }) {
const value = useMemo(() => {
console.log('Computing...');
return a + b;
}, [a, b]);
return <div>{value}</div>;
}
// mount时:
// 1. 执行nextCreate() 计算 a + b
// 2. 保存 [value, [a, b]] 到 hook.memoizedState
// 3. 返回 value
`;2.2 update阶段
typescript
/**
* update阶段的useMemo
*/
function updateMemo<T>(
nextCreate: () => T,
deps: Array<any> | void | null
): T {
// 获取当前Hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<any> | null = prevState[1];
// 比较依赖
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖未变,返回缓存值
return prevState[0];
}
}
}
// 依赖变化,重新计算
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
/**
* 比较依赖数组
*/
function areHookInputsEqual(
nextDeps: Array<any>,
prevDeps: Array<any> | null
): boolean {
if (prevDeps === null) {
return false;
}
// 浅比较每个依赖项
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}2.3 依赖比较详解
typescript
// 依赖比较示例
const dependencyComparisonExamples = {
基本类型: {
代码: `
const value = useMemo(() => {
return a + b;
}, [a, b]);
`,
比较: `
Object.is(1, 1) // true
Object.is('hello', 'hello') // true
Object.is(true, true) // true
`
},
对象引用: {
代码: `
const obj1 = { id: 1 };
const obj2 = { id: 1 };
Object.is(obj1, obj2) // false (不同引用)
Object.is(obj1, obj1) // true (相同引用)
`,
问题: `
function Component() {
const config = { threshold: 10 }; // 每次渲染都创建新对象
const value = useMemo(() => {
return compute(config);
}, [config]); // config每次都是新引用,缓存失效
}
`,
解决: `
// 方案1: 提取到组件外
const config = { threshold: 10 };
function Component() {
const value = useMemo(() => {
return compute(config);
}, []); // config稳定
}
// 方案2: 嵌套useMemo
function Component() {
const config = useMemo(() => ({ threshold: 10 }), []);
const value = useMemo(() => {
return compute(config);
}, [config]); // config稳定
}
// 方案3: 解构需要的值
function Component({ user }) {
const userId = user.id;
const value = useMemo(() => {
return compute(userId);
}, [userId]); // 只依赖id
}
`
},
数组: {
代码: `
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
Object.is(arr1, arr2) // false
`,
注意: '数组也是按引用比较,需要保持引用稳定'
},
函数: {
代码: `
const fn1 = () => {};
const fn2 = () => {};
Object.is(fn1, fn2) // false
`,
建议: '函数依赖应该使用useCallback包裹'
}
};3. useCallback实现
3.1 mount阶段
typescript
/**
* mount阶段的useCallback
*/
function mountCallback<T>(
callback: T,
deps: Array<any> | void | null
): T {
// 创建Hook节点
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 保存函数和依赖
hook.memoizedState = [callback, nextDeps];
return callback;
}
// 使用示例
const useCallbackExample = `
function Component() {
const [count, setCount] = useState(0);
const handleClick = useCallback(() => {
console.log('Clicked', count);
}, [count]);
return <Button onClick={handleClick} />;
}
// mount时:
// 1. 保存 [handleClick, [count]] 到 hook.memoizedState
// 2. 返回 handleClick
`;3.2 update阶段
typescript
/**
* update阶段的useCallback
*/
function updateCallback<T>(
callback: T,
deps: Array<any> | void | null
): T {
// 获取当前Hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<any> | null = prevState[1];
// 比较依赖
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖未变,返回缓存的函数
return prevState[0];
}
}
}
// 依赖变化,保存新函数
hook.memoizedState = [callback, nextDeps];
return callback;
}3.3 useCallback与useMemo的关系
typescript
// useCallback是useMemo的语法糖
const callbackMemoRelation = {
等价关系: `
// 这两者完全等价:
const callback = useCallback(fn, deps);
const callback = useMemo(() => fn, deps);
`,
实现对比: `
// useCallback实现
function useCallback(callback, deps) {
return useMemo(() => callback, deps);
}
// 或者独立实现(更高效)
function useCallback(callback, deps) {
const hook = updateWorkInProgressHook();
// ... 直接保存callback
hook.memoizedState = [callback, deps];
return callback;
}
`,
选择: {
useCallback: '缓存函数',
useMemo: '缓存计算结果或其他值'
}
};4. 性能优化场景
4.1 避免子组件重渲染
typescript
// 场景:子组件使用React.memo
const avoidRerender = {
问题代码: `
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ❌ 每次渲染都创建新函数
const handleClick = () => {
console.log(count);
};
return (
<div>
<Child onClick={handleClick} />
<button onClick={() => setName('New')}>Change Name</button>
</div>
);
}
const Child = React.memo(({ onClick }) => {
console.log('Child render');
return <button onClick={onClick}>Click</button>;
});
// 问题: name变化时,Child也重渲染
// 因为handleClick是新引用
`,
优化代码: `
function Parent() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ✓ 使用useCallback缓存函数
const handleClick = useCallback(() => {
console.log(count);
}, [count]);
return (
<div>
<Child onClick={handleClick} />
<button onClick={() => setName('New')}>Change Name</button>
</div>
);
}
const Child = React.memo(({ onClick }) => {
console.log('Child render');
return <button onClick={onClick}>Click</button>;
});
// name变化时,handleClick引用不变,Child不重渲染
`
};4.2 昂贵计算缓存
typescript
// 场景:复杂计算
const expensiveComputation = {
问题代码: `
function Component({ items }) {
const [filter, setFilter] = useState('');
// ❌ 每次渲染都计算
const filteredItems = items.filter(item =>
item.name.includes(filter)
).sort((a, b) =>
a.name.localeCompare(b.name)
).map(item => ({
...item,
processed: true
}));
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<List items={filteredItems} />
</div>
);
}
// 问题: 即使filter没变,每次渲染都重新计算
`,
优化代码: `
function Component({ items }) {
const [filter, setFilter] = useState('');
// ✓ 使用useMemo缓存计算结果
const filteredItems = useMemo(() => {
console.log('Computing filtered items...');
return items.filter(item =>
item.name.includes(filter)
).sort((a, b) =>
a.name.localeCompare(b.name)
).map(item => ({
...item,
processed: true
}));
}, [items, filter]);
return (
<div>
<input value={filter} onChange={e => setFilter(e.target.value)} />
<List items={filteredItems} />
</div>
);
}
// 只有items或filter变化时才重新计算
`
};4.3 引用稳定性
typescript
// 场景:useEffect依赖
const referenceStability = {
问题代码: `
function Component({ id }) {
const [data, setData] = useState(null);
// ❌ 每次渲染都创建新对象
const options = {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};
useEffect(() => {
fetchData(id, options).then(setData);
}, [id, options]); // options每次都变,effect总是执行
return <div>{data}</div>;
}
`,
优化代码: `
function Component({ id }) {
const [data, setData] = useState(null);
// ✓ 使用useMemo保持引用稳定
const options = useMemo(() => ({
method: 'GET',
headers: { 'Content-Type': 'application/json' }
}), []);
useEffect(() => {
fetchData(id, options).then(setData);
}, [id, options]); // options稳定,只在id变化时执行
return <div>{data}</div>;
}
// 或者移除options依赖
function Component({ id }) {
const [data, setData] = useState(null);
useEffect(() => {
const options = {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
};
fetchData(id, options).then(setData);
}, [id]); // 只依赖id
return <div>{data}</div>;
}
`
};5. 常见陷阱
5.1 过度使用
typescript
const overuse = {
问题: '不是所有场景都需要useMemo/useCallback',
不必要的优化: `
function Component() {
// ❌ 不必要:基本类型计算很快
const sum = useMemo(() => 1 + 2, []);
// ❌ 不必要:简单字符串拼接
const message = useMemo(() => 'Hello ' + name, [name]);
// ❌ 不必要:子组件没有优化
const handleClick = useCallback(() => {
console.log('click');
}, []);
return <div onClick={handleClick}>{message}</div>;
// div不会做props比较,useCallback无意义
}
`,
应该使用的场景: [
'1. 昂贵的计算',
'2. 子组件使用React.memo',
'3. 依赖数组中的引用类型',
'4. 自定义Hook返回的值',
'5. Context value对象'
],
性能开销: `
useMemo/useCallback本身也有开销:
- 创建闭包
- 存储依赖
- 比较依赖
如果计算很简单,可能还不如直接计算
`
};5.2 依赖遗漏
typescript
const missingDependencies = {
问题代码: `
function Component() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);
// ❌ 错误:遗漏multiplier依赖
const result = useMemo(() => {
return count * multiplier;
}, [count]); // 应该包含multiplier
// multiplier变化时,result不会更新
}
`,
正确代码: `
const result = useMemo(() => {
return count * multiplier;
}, [count, multiplier]); // ✓ 包含所有依赖
`,
ESLint规则: `
// 使用 eslint-plugin-react-hooks
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/exhaustive-deps": "warn"
}
}
// 会警告遗漏的依赖
`
};5.3 闭包陷阱
typescript
const closureTrap = {
问题代码: `
function Component() {
const [count, setCount] = useState(0);
// ❌ 空依赖导致闭包问题
const handleClick = useCallback(() => {
console.log(count); // 总是打印初始值0
setCount(count + 1); // 总是设置为1
}, []); // 闭包捕获初始count
return (
<button onClick={handleClick}>
Count: {count}
</button>
);
}
`,
解决方案1: `
// 添加依赖
const handleClick = useCallback(() => {
console.log(count);
setCount(count + 1);
}, [count]); // count变化时更新函数
`,
解决方案2: `
// 使用函数式更新
const handleClick = useCallback(() => {
setCount(c => {
console.log(c); // 获取最新值
return c + 1;
});
}, []); // 不依赖count
`,
解决方案3: `
// 使用ref
const countRef = useRef(count);
countRef.current = count;
const handleClick = useCallback(() => {
console.log(countRef.current); // 最新值
setCount(c => c + 1);
}, []);
`
};6. 最佳实践
6.1 何时使用
typescript
const whenToUse = {
useMemo适用场景: [
{
场景: '昂贵计算',
示例: `
const result = useMemo(() => {
return complexCalculation(data);
}, [data]);
`
},
{
场景: '引用稳定性',
示例: `
const options = useMemo(() => ({
method: 'GET'
}), []);
useEffect(() => {
fetch(url, options);
}, [url, options]);
`
},
{
场景: '避免子组件重渲染',
示例: `
const items = useMemo(() =>
data.map(transform),
[data]
);
<MemoizedList items={items} />
`
}
],
useCallback适用场景: [
{
场景: '传递给优化的子组件',
示例: `
const handleClick = useCallback(() => {
doSomething();
}, []);
<MemoizedButton onClick={handleClick} />
`
},
{
场景: '作为useEffect依赖',
示例: `
const fetchData = useCallback(() => {
fetch(url);
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
`
},
{
场景: '自定义Hook返回',
示例: `
function useCustomHook() {
const handler = useCallback(() => {
// ...
}, []);
return { handler };
}
`
}
],
不需要使用的场景: [
'基本类型计算',
'简单字符串操作',
'子组件没有优化',
'组件本身很简单',
'不在依赖数组中的引用'
]
};6.2 依赖管理
typescript
const dependencyManagement = {
规则: [
'1. 包含所有使用的外部变量',
'2. 不要遗漏依赖',
'3. 不要在依赖中使用对象/数组字面量',
'4. 考虑使用useRef减少依赖'
],
技巧: {
解构props: `
// ✓ 好:解构需要的值
function Component({ user }) {
const userName = user.name;
const greeting = useMemo(() => {
return \`Hello, \${userName}\`;
}, [userName]); // 只依赖name
}
`,
提取常量: `
// 组件外定义常量
const CONFIG = { threshold: 10 };
function Component() {
const value = useMemo(() => {
return compute(CONFIG);
}, []); // CONFIG是常量,不需要依赖
}
`,
使用ref: `
function Component({ onChange }) {
const onChangeRef = useRef(onChange);
onChangeRef.current = onChange;
const handler = useCallback(() => {
onChangeRef.current();
}, []); // 不依赖onChange
}
`
}
};7. 性能测试
typescript
// 性能对比测试
const performanceTest = {
测试代码: `
function TestComponent({ items }) {
const [count, setCount] = useState(0);
// 不使用useMemo
console.time('without useMemo');
const result1 = items.filter(item => item.active)
.map(item => item.value)
.reduce((sum, val) => sum + val, 0);
console.timeEnd('without useMemo');
// 使用useMemo
console.time('with useMemo');
const result2 = useMemo(() => {
return items.filter(item => item.active)
.map(item => item.value)
.reduce((sum, val) => sum + val, 0);
}, [items]);
console.timeEnd('with useMemo');
return (
<div>
<p>Result: {result2}</p>
<button onClick={() => setCount(c => c + 1)}>
Trigger Re-render: {count}
</button>
</div>
);
}
`,
结果分析: `
首次渲染:
- without useMemo: 0.5ms
- with useMemo: 0.6ms (稍慢,因为有额外的缓存逻辑)
count变化导致的重渲染(items未变):
- without useMemo: 0.5ms (每次重新计算)
- with useMemo: 0.01ms (使用缓存值)
结论:
- 对于昂贵计算,useMemo有明显优势
- 对于简单计算,可能还不如直接计算
`
};8. 面试高频问题
typescript
const interviewQA = {
Q1: {
question: 'useMemo和useCallback的区别?',
answer: [
'1. useMemo缓存计算结果',
'2. useCallback缓存函数引用',
'3. useCallback等价于useMemo(() => fn, deps)',
'4. 都用于性能优化',
'5. 都使用浅比较依赖'
]
},
Q2: {
question: 'useMemo的实现原理?',
answer: `
1. mount时执行计算函数获取值
2. 保存[value, deps]到hook.memoizedState
3. update时比较依赖
4. 依赖未变返回缓存值
5. 依赖变化重新计算
`
},
Q3: {
question: '何时使用useMemo?',
answer: [
'1. 昂贵的计算',
'2. 需要引用稳定性',
'3. 传递给React.memo组件',
'4. 作为其他Hook的依赖',
'5. Context value对象'
]
},
Q4: {
question: 'useMemo一定能提升性能吗?',
answer: `
不一定:
useMemo本身有开销:
- 创建闭包
- 存储依赖
- 比较依赖
只有当计算成本 > useMemo开销时,
才真正提升性能
简单计算可能还不如直接计算
`
},
Q5: {
question: 'useCallback的闭包陷阱如何避免?',
answer: [
'1. 正确添加依赖',
'2. 使用函数式更新',
'3. 使用useRef存储最新值',
'4. 使用useReducer',
'5. 启用ESLint规则检查'
]
},
Q6: {
question: '如何优化大列表渲染?',
answer: `
1. 使用React.memo包裹列表项
2. useCallback缓存事件处理器
3. useMemo缓存过滤/排序结果
4. 虚拟滚动(react-window)
5. 分页加载
6. key使用稳定ID
`
}
};9. 总结
useMemo和useCallback手写实现的核心要点:
- 数据结构: [value, deps]存储在hook.memoizedState
- 依赖比较: Object.is浅比较
- 缓存机制: 依赖未变返回缓存值
- 性能权衡: 不是所有场景都需要
- 最佳实践: 昂贵计算、引用稳定、子组件优化
- 陷阱: 过度使用、依赖遗漏、闭包问题
- 关系: useCallback是useMemo的特例
理解useMemo和useCallback是React性能优化的关键。