Appearance
手写useMemo和useCallback
学习目标
通过本章学习,你将深入理解:
- useMemo的内部实现原理
- useCallback的内部实现原理
- 两者的关系和区别
- 依赖数组的比较机制
- 缓存策略的实现
- 性能优化的原理
- 手写简化版实现
- React 19的优化改进
第一部分:useMemo和useCallback基础回顾
1.1 useMemo的使用
jsx
function Component({ items }) {
// 缓存计算结果
const expensiveValue = useMemo(() => {
console.log('计算中...');
return items
.filter(item => item.active)
.reduce((sum, item) => sum + item.value, 0);
}, [items]);
return <div>总计: {expensiveValue}</div>;
}1.2 useCallback的使用
jsx
function Component() {
const [count, setCount] = useState(0);
// 缓存函数引用
const handleClick = useCallback(() => {
console.log('点击:', count);
}, [count]);
return <Child onClick={handleClick} />;
}1.3 两者的关系
javascript
// useCallback是useMemo的特殊形式
useCallback(fn, deps)
// 等价于
useMemo(() => fn, deps)
// 示例:
const callback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
// 等同于:
const callback = useMemo(() => {
return () => {
doSomething(a, b);
};
}, [a, b]);第二部分:核心数据结构
2.1 Hook对象结构
javascript
// useMemo/useCallback的Hook对象
type Hook = {
// 存储 [缓存的值, 依赖数组]
memoizedState: [any, Array<any>] | null,
baseState: null,
baseQueue: null,
queue: null,
// 指向下一个Hook
next: Hook | null
};
// 示例:
// useMemo(() => computeExpensiveValue(), [dep1, dep2])
// Hook.memoizedState = [computedValue, [dep1, dep2]]
// useCallback((a, b) => doSomething(a, b), [a, b])
// Hook.memoizedState = [callbackFunction, [a, b]]2.2 依赖比较机制
javascript
// React使用Object.is进行依赖比较
function areHookInputsEqual(nextDeps, prevDeps) {
// 没有依赖数组,总是更新
if (prevDeps === null) {
return false;
}
// 依赖数量不同,更新
if (nextDeps.length !== prevDeps.length) {
return false;
}
// 逐个比较依赖项
for (let i = 0; i < prevDeps.length; i++) {
// 使用Object.is进行比较(浅比较)
if (Object.is(nextDeps[i], prevDeps[i])) {
continue;
}
return false; // 有任何一项不同就返回false
}
return true; // 所有项都相同
}
// Object.is的特点:
Object.is(0, -0); // false(与===不同)
Object.is(NaN, NaN); // true(与===不同)
Object.is({}, {}); // false(对象比较引用)
Object.is([1], [1]); // false(数组比较引用)第三部分:mountMemo实现
3.1 初始化阶段
javascript
// 简化的mountMemo实现
function mountMemo(nextCreate, deps) {
// 1. 创建Hook对象
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 2. 执行计算函数,获取初始值
const nextValue = nextCreate();
// 3. 存储值和依赖
hook.memoizedState = [nextValue, nextDeps];
// 4. 返回计算结果
return nextValue;
}
// mountWorkInProgressHook的实现(复用)
function mountWorkInProgressHook() {
const hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// 第一个Hook
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// 后续Hook
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}3.2 初始化示例
javascript
function Component({ items }) {
// useMemo 1
const total = useMemo(() => {
return items.reduce((sum, item) => sum + item.value, 0);
}, [items]);
// useMemo 2
const average = useMemo(() => {
return total / items.length;
}, [total, items.length]);
return <div>{average}</div>;
}
// 执行流程:
// 1. 调用第一个useMemo
// → mountMemo
// → 创建Hook1
// → 执行 () => items.reduce(...)
// → 计算得到 total = 100
// → Hook1.memoizedState = [100, [items]]
// → 返回 100
// 2. 调用第二个useMemo
// → mountMemo
// → 创建Hook2
// → 执行 () => total / items.length
// → 计算得到 average = 20
// → Hook2.memoizedState = [20, [total, items.length]]
// → 返回 20
// Fiber.memoizedState → Hook1 → Hook2 → null第四部分:updateMemo实现
4.1 更新阶段
javascript
// 简化的updateMemo实现
function updateMemo(nextCreate, deps) {
// 1. 获取对应的Hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 2. 获取上次缓存的值和依赖
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1];
// 3. 比较依赖是否变化
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖未变,返回缓存的值
return prevState[0];
}
}
}
// 4. 依赖变化,重新计算
const nextValue = nextCreate();
// 5. 更新缓存
hook.memoizedState = [nextValue, nextDeps];
// 6. 返回新值
return nextValue;
}
// updateWorkInProgressHook的实现(复用)
function updateWorkInProgressHook() {
// 从current树获取对应Hook
let nextCurrentHook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = currentHook.next;
}
currentHook = nextCurrentHook;
// 创建workInProgress Hook
const newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null
};
// 添加到链表
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
return workInProgressHook;
}4.2 更新流程示例
javascript
function Component({ count }) {
const doubled = useMemo(() => {
console.log('计算doubled');
return count * 2;
}, [count]);
const tripled = useMemo(() => {
console.log('计算tripled');
return count * 3;
}, [count]);
return <div>{doubled} - {tripled}</div>;
}
// 首次渲染(count = 1):
// → 计算doubled: 1 * 2 = 2
// → Hook1.memoizedState = [2, [1]]
// → 计算tripled: 1 * 3 = 3
// → Hook2.memoizedState = [3, [1]]
// 第二次渲染(count = 1,未变化):
// → updateMemo(doubled)
// → 比较deps: [1] === [1] ✅
// → 返回缓存值 2(不计算)
// → updateMemo(tripled)
// → 比较deps: [1] === [1] ✅
// → 返回缓存值 3(不计算)
// 第三次渲染(count = 2,变化):
// → updateMemo(doubled)
// → 比较deps: [2] !== [1] ❌
// → 重新计算: 2 * 2 = 4
// → Hook1.memoizedState = [4, [2]]
// → updateMemo(tripled)
// → 比较deps: [2] !== [1] ❌
// → 重新计算: 2 * 3 = 6
// → Hook2.memoizedState = [6, [2]]第五部分:useCallback实现
5.1 mountCallback和updateCallback
javascript
// mountCallback的实现
function mountCallback(callback, deps) {
// 创建Hook
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 直接存储callback和deps
hook.memoizedState = [callback, nextDeps];
// 返回callback
return callback;
}
// updateCallback的实现
function updateCallback(callback, deps) {
// 获取Hook
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
// 获取上次的callback和deps
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps = prevState[1];
// 比较依赖
if (areHookInputsEqual(nextDeps, prevDeps)) {
// 依赖未变,返回旧callback
return prevState[0];
}
}
}
// 依赖变化,存储新callback
hook.memoizedState = [callback, nextDeps];
// 返回新callback
return callback;
}5.2 useCallback与useMemo的区别
javascript
// 实现对比
function useMemo(create, deps) {
// 执行create函数,缓存返回值
const value = create();
return value;
}
function useCallback(callback, deps) {
// 直接缓存callback函数本身
return callback;
}
// 使用对比
const value = useMemo(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
// value是计算结果
const callback = useCallback(() => {
return computeExpensiveValue(a, b);
}, [a, b]);
// callback是函数本身
// useCallback可以用useMemo实现
const callback = useMemo(() => {
return () => {
return computeExpensiveValue(a, b);
};
}, [a, b]);第六部分:简化版实现
6.1 完整的简化实现
javascript
// 全局变量
let currentFiber = null;
let hookIndex = 0;
// 简化的useMemo实现
function useMemo(create, deps) {
const fiber = currentFiber;
const hooks = fiber.hooks || (fiber.hooks = []);
const index = hookIndex++;
// 初始化Hook
if (!hooks[index]) {
// 首次渲染,执行create并缓存
const value = create();
hooks[index] = {
value,
deps
};
return value;
}
const hook = hooks[index];
// 比较依赖
const hasChanged = !deps || !hook.deps ||
deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
if (hasChanged) {
// 依赖变化,重新计算
const value = create();
hook.value = value;
hook.deps = deps;
return value;
}
// 依赖未变,返回缓存值
return hook.value;
}
// 简化的useCallback实现
function useCallback(callback, deps) {
const fiber = currentFiber;
const hooks = fiber.hooks || (fiber.hooks = []);
const index = hookIndex++;
// 初始化Hook
if (!hooks[index]) {
hooks[index] = {
callback,
deps
};
return callback;
}
const hook = hooks[index];
// 比较依赖
const hasChanged = !deps || !hook.deps ||
deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
if (hasChanged) {
// 依赖变化,使用新callback
hook.callback = callback;
hook.deps = deps;
return callback;
}
// 依赖未变,返回旧callback
return hook.callback;
}
// 使用示例
function createComponent(renderFn) {
const fiber = {
hooks: null,
render: () => {
hookIndex = 0;
currentFiber = fiber;
return renderFn();
}
};
return fiber;
}
// 组件
const ComponentFiber = createComponent(() => {
const [count, setCount] = useState(0);
// useMemo示例
const doubled = useMemo(() => {
console.log('计算doubled');
return count * 2;
}, [count]);
// useCallback示例
const handleClick = useCallback(() => {
console.log('点击count:', count);
}, [count]);
console.log('渲染 count:', count, 'doubled:', doubled);
return {
count,
doubled,
handleClick,
increment: () => setCount(count + 1)
};
});
// 初始渲染
let result = ComponentFiber.render();
// 输出: 计算doubled
// 渲染 count: 0 doubled: 0
// 第一次更新(count变化)
result.increment();
// 输出: 计算doubled
// 渲染 count: 1 doubled: 2
// 第二次渲染(count未变)
ComponentFiber.render();
// 输出: 渲染 count: 1 doubled: 2
// 注意:没有"计算doubled",使用了缓存6.2 增强版:支持无依赖数组
javascript
// 增强的useMemo
function useMemo(create, deps) {
const fiber = currentFiber;
const hooks = fiber.hooks || (fiber.hooks = []);
const index = hookIndex++;
if (!hooks[index]) {
const value = create();
hooks[index] = {
value,
deps,
hasValue: true
};
return value;
}
const hook = hooks[index];
// 处理不同的deps情况
let shouldUpdate = false;
if (deps === undefined) {
// 无deps参数,每次都更新
shouldUpdate = true;
} else if (deps === null) {
// deps为null,永不更新
shouldUpdate = false;
} else if (!hook.deps) {
// 上次没有deps,这次有,更新
shouldUpdate = true;
} else if (deps.length !== hook.deps.length) {
// deps长度变化,更新
shouldUpdate = true;
} else {
// 比较每个依赖项
shouldUpdate = deps.some((dep, i) => !Object.is(dep, hook.deps[i]));
}
if (shouldUpdate) {
const value = create();
hook.value = value;
hook.deps = deps;
return value;
}
return hook.value;
}第七部分:性能优化原理
7.1 useMemo的优化效果
javascript
// 未优化版本
function UnoptimizedComponent({ items }) {
// 每次渲染都重新计算
const total = items.reduce((sum, item) => sum + item.value, 0);
const average = total / items.length;
const maximum = Math.max(...items.map(item => item.value));
return <div>{total} - {average} - {maximum}</div>;
}
// 优化版本
function OptimizedComponent({ items }) {
// 只在items变化时重新计算
const total = useMemo(() => {
console.log('计算total');
return items.reduce((sum, item) => sum + item.value, 0);
}, [items]);
const average = useMemo(() => {
console.log('计算average');
return total / items.length;
}, [total, items.length]);
const maximum = useMemo(() => {
console.log('计算maximum');
return Math.max(...items.map(item => item.value));
}, [items]);
return <div>{total} - {average} - {maximum}</div>;
}
// 性能对比:
// 场景1:items未变,但父组件重新渲染
// 未优化:重复计算3次
// 优化:使用缓存,0次计算
// 场景2:只有items.length变化(内容未变)
// 未优化:重复计算3次
// 优化:只重新计算average,total和maximum使用缓存7.2 useCallback的优化效果
javascript
// 未优化版本
function UnoptimizedParent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 每次渲染都创建新函数
const handleClick = () => {
console.log('clicked');
};
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
{/* text变化时,Child会重新渲染,即使handleClick逻辑未变 */}
<MemoChild onClick={handleClick} />
</div>
);
}
// 优化版本
function OptimizedParent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// 缓存函数引用
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
{/* text变化时,handleClick引用不变,Child不重新渲染 */}
<MemoChild onClick={handleClick} />
</div>
);
}
const MemoChild = React.memo(function Child({ onClick }) {
console.log('Child渲染');
return <button onClick={onClick}>Click</button>;
});第八部分:React 19的优化
8.1 自动缓存优化
javascript
// React 19编译器可能自动优化
// 开发者编写的代码
function Component({ items }) {
const filtered = items.filter(item => item.active);
const total = filtered.reduce((sum, item) => sum + item.value, 0);
return <div>{total}</div>;
}
// 编译器可能转换为
function Component({ items }) {
const filtered = useMemo(() => {
return items.filter(item => item.active);
}, [items]);
const total = useMemo(() => {
return filtered.reduce((sum, item) => sum + item.value, 0);
}, [filtered]);
return <div>{total}</div>;
}8.2 智能依赖追踪
javascript
// React 19可能支持更智能的依赖追踪
// 开发者代码
function Component({ user }) {
const greeting = useMemo(() => {
return `Hello, ${user.name}`;
}, [user]); // 依赖整个user对象
// React 19可能自动优化为只依赖user.name
// 内部实现可能类似:
// const greeting = useMemo(() => {
// return `Hello, ${user.name}`;
// }, [user.name]);
}注意事项
1. 不要过度使用
javascript
// ❌ 过度优化:简单计算不需要useMemo
const doubled = useMemo(() => count * 2, [count]);
// ✅ 简单计算直接执行
const doubled = count * 2;
// ✅ 复杂计算才使用useMemo
const expensive = useMemo(() => {
return complexCalculation(data);
}, [data]);2. 依赖数组必须完整
javascript
// ❌ 错误:缺少依赖
const result = useMemo(() => {
return a + b + c;
}, [a, b]); // 缺少c
// ✅ 正确:包含所有依赖
const result = useMemo(() => {
return a + b + c;
}, [a, b, c]);3. 对象和数组依赖
javascript
// ❌ 问题:对象每次都是新的
function Parent() {
const config = { api: 'url' }; // 每次渲染都创建新对象
const data = useMemo(() => {
return fetchData(config);
}, [config]); // config每次都不同,useMemo失效
}
// ✅ 解决方案1:提取到组件外
const CONFIG = { api: 'url' };
function Parent() {
const data = useMemo(() => {
return fetchData(CONFIG);
}, []);
}
// ✅ 解决方案2:useMemo包裹config
function Parent() {
const config = useMemo(() => ({ api: 'url' }), []);
const data = useMemo(() => {
return fetchData(config);
}, [config]);
}4. useCallback必须配合React.memo
javascript
// ❌ 无效:子组件没有memo
function Parent() {
const handleClick = useCallback(() => {}, []);
return <Child onClick={handleClick} />; // Child没有memo,每次都渲染
}
// ✅ 有效:子组件使用memo
function Parent() {
const handleClick = useCallback(() => {}, []);
return <MemoChild onClick={handleClick} />;
}
const MemoChild = React.memo(Child);常见问题
Q1: useMemo和useCallback什么时候使用?
A:
useMemo: 缓存计算结果
- 复杂计算
- 引用相等性重要
useCallback: 缓存函数引用
- 传给memo组件
- 作为依赖使用
javascript
// useMemo
const expensive = useMemo(() => heavyCalculation(), [deps]);
// useCallback
const callback = useCallback(() => {}, [deps]);
return <MemoChild onClick={callback} />;Q2: 为什么使用了useMemo性能反而变差?
A: 可能原因:
- 计算本身不昂贵
- 依赖频繁变化
- useMemo本身有开销
javascript
// ❌ 性能更差:简单计算 + useMemo开销
const sum = useMemo(() => a + b, [a, b]);
// ✅ 更好:直接计算
const sum = a + b;Q3: useMemo可以替代所有计算吗?
A: 不应该。遵循原则:
javascript
// 先写可读的代码
const value = compute();
// 发现性能问题时才优化
const value = useMemo(() => compute(), [deps]);Q4: useCallback(fn, []) 和 直接定义函数有什么区别?
A:
javascript
// 直接定义:每次渲染创建新函数
const fn = () => {};
// useCallback:函数引用保持不变
const fn = useCallback(() => {}, []);
// 区别只在引用相等性:
// 直接定义:fn1 !== fn2
// useCallback:fn1 === fn2总结
实现要点
- 数据结构: Hook.memoizedState = [value, deps]
- 初始化: 执行计算/保存函数
- 更新: 比较deps决定是否更新
- 缓存策略: 浅比较依赖数组
- 性能权衡: 缓存收益 > 比较成本
useMemo vs useCallback
useMemo(create, deps)
↓
执行create函数
↓
缓存返回值
↓
返回值
useCallback(fn, deps)
↓
直接缓存fn
↓
返回fn使用决策树
需要缓存吗?
├─ 缓存计算结果 → useMemo
├─ 缓存函数引用 → useCallback
└─ 简单值 → 不需要缓存
配合React.memo?
├─ 是 → 使用useCallback
└─ 否 → 可能不需要
计算昂贵?
├─ 是 → 使用useMemo
└─ 否 → 不需要最佳实践
javascript
// 1. 只优化瓶颈
// 2. 使用Profiler测量
// 3. 保持依赖完整
// 4. 避免依赖不稳定对象
// 5. 配合React.memo使用理解useMemo和useCallback的实现原理,能帮助我们更明智地使用它们,真正提升应用性能!