阅读视图

发现新文章,点击刷新页面。

React Fiber 架构与渲染流程

React 的 Fiber 架构是 React 16 中引入的重大重构,它彻底改变了 React 的渲染机制,为并发特性(如 Concurrent Mode)奠定了基础。

为什么需要 Fiber 架构?

传统 Stack Reconciler 的局限性

在 React 16 之前,React 使用栈协调器(Stack Reconciler),其存在以下问题:

  1. 不可中断的递归遍历:渲染过程是同步、不可中断的
  2. 阻塞主线程:大型组件树会导致界面卡顿
  3. 无法优先处理高优先级更新:所有更新同等对待

Fiber 架构的解决方案

Fiber 架构引入了:

  1. 可中断的渲染过程:将工作分解为小单元
  2. 优先级调度:不同更新有不同的优先级
  3. 并发渲染能力:为 Concurrent Mode 提供基础

Fiber 节点的核心结构

Fiber 是 React 的最小工作单元,每个组件对应一个 Fiber 节点:

// Fiber 节点结构(简化版)
type Fiber = {
  // 标识信息
  tag: WorkTag,           // 组件类型(函数组件、类组件、宿主组件等)
  key: null | string,     // 唯一标识
  type: any,              // 组件函数/类或DOM标签名
  
  // 树结构信息
  return: Fiber | null,   // 父节点
  child: Fiber | null,    // 第一个子节点
  sibling: Fiber | null,  // 下一个兄弟节点
  
  // 状态信息
  pendingProps: any,      // 新的 props
  memoizedProps: any,     // 上一次渲染的 props
  memoizedState: any,     // 上一次渲染的状态(hooks、state等)
  stateNode: any,         // 对应的实例(DOM节点、组件实例)
  
  // 副作用相关
  flags: Flags,           // 需要执行的副作用标记(增、删、更新)
  subtreeFlags: Flags,    // 子树中的副作用标记
  deletions: Fiber[] | null, // 待删除的子节点
  
  // 工作进度相关
  alternate: Fiber | null, // 上一次渲染的fiber节点(用于diff)
  lanes: Lanes,           // 优先级车道
  childLanes: Lanes,      // 子节点的优先级车道
  
  // Hook 相关(函数组件)
  memoizedState: any,     // Hook 链表头
};

Fiber 树的双缓存机制

React 使用双缓存技术来避免渲染过程中的视觉闪烁:

  1. Current Tree:当前屏幕上显示内容对应的 Fiber 树
  2. WorkInProgress Tree:正在构建的新 Fiber 树
// 双缓存工作机制
function updateComponent() {
  // 从当前fiber创建workInProgress fiber
  const current = currentlyRenderingFiber.alternate;
  if (current !== null) {
    // 复用现有的fiber节点
    workInProgress = createWorkInProgress(current, pendingProps);
  } else {
    // 创建新的fiber节点
    workInProgress = createFiberFromTypeAndProps(
      // ...参数
    );
  }
  
  // 处理workInProgress树...
}

完整的渲染流程

React 的渲染过程分为两个主要阶段:

1. Render 阶段(可中断)

Render 阶段是异步、可中断的,负责计算变更:

// Render 阶段工作循环
function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork !== null && !shouldYield) {
    // 执行当前工作单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    
    // 检查是否需要让出主线程
    shouldYield = deadline.timeRemaining() < 1;
  }
  
  if (nextUnitOfWork !== null) {
    // 还有工作,稍后继续
    requestIdleCallback(workLoop);
  } else {
    // 所有工作完成,进入提交阶段
    commitRoot();
  }
}

performUnitOfWork 的深度优先遍历

function performUnitOfWork(fiber) {
  // 1. 开始工作:创建子fiber节点(调用组件render方法)
  const next = beginWork(fiber);
  
  if (next !== null) {
    return next; // 如果有子节点,返回子节点继续处理
  }
  
  // 2. 没有子节点,完成当前节点工作,转向兄弟节点或父节点
  let current = fiber;
  while (current !== null) {
    // 完成当前节点(生成effect列表等)
    completeWork(current);
    
    if (current.sibling !== null) {
      return current.sibling; // 处理兄弟节点
    }
    current = current.return; // 返回父节点
  }
  
  return null; // 遍历完成
}

beginWork:处理组件更新

function beginWork(fiber) {
  switch (fiber.tag) {
    case FunctionComponent:
      // 处理函数组件
      return updateFunctionComponent(fiber);
    case ClassComponent:
      // 处理类组件
      return updateClassComponent(fiber);
    case HostComponent:
      // 处理DOM元素
      return updateHostComponent(fiber);
    // ... 其他组件类型
  }
}

function updateFunctionComponent(fiber) {
  // 准备Hooks环境
  prepareToUseHooks(fiber);
  
  // 调用组件函数,获取子元素
  const children = fiber.type(fiber.pendingProps);
  
  // 协调子元素
  reconcileChildren(fiber, children);
  
  return fiber.child; // 返回第一个子节点
}

completeWork:完成节点处理

function completeWork(fiber) {
  switch (fiber.tag) {
    case HostComponent:
      // 处理DOM元素的属性更新等
      if (fiber.stateNode !== null) {
        // 更新现有的DOM节点
        updateDOMProperties(fiber.stateNode, fiber.memoizedProps, fiber.pendingProps);
      } else {
        // 创建新的DOM节点
        const instance = createInstance(fiber.type, fiber.pendingProps);
        fiber.stateNode = instance;
      }
      break;
    // ... 其他组件类型
  }
  
  // 收集effect到父节点
  if (fiber.flags !== NoFlags) {
    // 将当前fiber的effect添加到父节点的effect列表中
    let parent = fiber.return;
    while (parent !== null) {
      parent.subtreeFlags |= fiber.flags;
      parent = parent.return;
    }
  }
}

2. Commit 阶段(不可中断)

Commit 阶段是同步、不可中断的,负责将变更应用到DOM:

function commitRoot() {
  // 1. 预处理:调用getSnapshotBeforeUpdate等
  commitBeforeMutationEffects();
  
  // 2. 应用DOM变更
  commitMutationEffects();
  
  // 3. 将workInProgress树切换为current树
  root.current = finishedWork;
  
  // 4. 处理布局effect(如useLayoutEffect)
  commitLayoutEffects();
  
  // 5. 调度被动effect(useEffect)
  schedulePassiveEffects();
}

commitMutationEffects:处理DOM变更

function commitMutationEffects() {
  // 遍历effect列表,执行DOM操作
  while (nextEffect !== null) {
    const flags = nextEffect.flags;
    
    if (flags & Placement) {
      // 插入新节点
      commitPlacement(nextEffect);
    }
    if (flags & Update) {
      // 更新节点
      commitUpdate(nextEffect);
    }
    if (flags & Deletion) {
      // 删除节点
      commitDeletion(nextEffect);
    }
    
    nextEffect = nextEffect.nextEffect;
  }
}

优先级调度机制

Fiber 架构引入了优先级概念,确保高优先级更新优先处理:

// 优先级类型(简化)
const SyncLane = 0b0000000000000000000000000000001; // 同步优先级
const InputContinuousLane = 0b0000000000000000000000000000100; // 连续输入
const DefaultLane = 0b0000000000000000000000000010000; // 默认优先级

// 基于优先级的调度
function scheduleUpdateOnFiber(fiber, lane) {
  // 标记优先级
  fiber.lanes = mergeLanes(fiber.lanes, lane);
  
  // 调度更新
  if (lane === SyncLane) {
    // 同步更新,立即执行
    performSyncWorkOnRoot(root);
  } else {
    // 异步更新,根据优先级调度
    ensureRootIsScheduled(root);
  }
}

并发模式下的工作方式

在并发模式下,React 可以中断低优先级工作来处理高优先级更新:

// 高优先级更新中断低优先级工作
function handleUserInput() {
  // 高优先级更新(用户输入)
  scheduleUpdateOnFiber(root, InputContinuousLane);
  
  // 如果当前有低优先级渲染正在进行...
  // React 会中断它,先处理高优先级更新
}

// 被中断的工作可以稍后重新开始
function resumeInterruptedWork(interruptedFiber) {
  // 从中断的地方继续工作
  nextUnitOfWork = interruptedFiber;
  requestIdleCallback(workLoop);
}

错误处理机制

Fiber 架构改进了错误处理:

function renderRoot() {
  try {
    // 正常的渲染工作
    workLoop();
  } catch (error) {
    // 处理错误,寻找错误边界
    let fiber = nextUnitOfWork;
    while (fiber !== null) {
      if (fiber.tag === ClassComponent && 
          typeof fiber.type.getDerivedStateFromError === 'function') {
        // 找到错误边界组件
        captureError(fiber, error);
        break;
      }
      fiber = fiber.return;
    }
    
    // 重新尝试渲染
    restartRender();
  }
}

性能优化特性

1. 增量渲染

Fiber 将渲染工作分解为小单元,可以分段完成:

// 时间分片示例
function workLoopConcurrent(deadline) {
  while (nextUnitOfWork !== null && deadline.timeRemaining() > 0) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
  }
  
  if (nextUnitOfWork !== null) {
    // 还有工作,稍后继续
    requestIdleCallback(workLoopConcurrent);
  }
}

2. 子树渲染跳过

当 props 未变化时,可以跳过整个子树的渲染:

function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
  if (current !== null) {
    // 检查props是否变化
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    
    if (oldProps === newProps && !hasContextChanged()) {
      // props未变化,可以跳过整个子树
      return null;
    }
  }
  
  // 需要重新渲染
  return updateFunctionComponent(workInProgress);
}

总结

React Fiber 架构的核心创新:

  1. 可中断的异步渲染:将渲染工作分解为小单元,可以暂停和恢复
  2. 优先级调度:不同更新有不同优先级,确保用户体验流畅
  3. 双缓存机制:避免渲染过程中的视觉闪烁
  4. 增量提交:DOM 变更分批进行,减少布局抖动
  5. 更好的错误处理:完善的错误边界机制

参考: incepter.github.io/how-react-w…

为什么说 useCallback 实际上是 useMemo 的特例

useCallback 是 useMemo 的特例,主要是因为它们在实现机制、设计理念和最终目的上高度一致,只是应用的场景和返回的值类型不同

1. 从概念等价性来看

从概念上讲,useCallback 完全可以由 useMemo 来实现:

// useCallback 的等效 useMemo 实现
const useCallbackEquivalent = (callback, deps) => {
  return useMemo(() => callback, deps);
};

// React 中的实际 useCallback 实现也是类似的思路
function useCallback(callback, deps) {
  return useMemo(() => callback, deps);
}

你看,useCallback 本质上就是:"记忆一个函数,当依赖项不变时返回相同的函数引用"。这正是 useMemo 所能做的事情——记忆任何类型的值,包括函数。

2. 从实现源码来看

虽然 React 源码中 useCallbackuseMemo 有各自的实现函数,但它们的逻辑结构几乎完全相同:

useMemo 的实现核心

function updateMemo(create, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null && nextDeps !== null) {
    if (areHookInputsEqual(nextDeps, prevState[1])) {
      return prevState[0]; // 返回缓存的值
    }
  }
  
  const nextValue = create(); // 重新计算
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

useCallback 的实现核心

function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null && nextDeps !== null) {
    if (areHookInputsEqual(nextDeps, prevState[1])) {
      return prevState[0]; // 返回缓存的函数
    }
  }
  
  hook.memoizedState = [callback, nextDeps];
  return callback; // "计算"就是直接返回函数本身
}

注意两者的高度相似性:

  1. 相同的依赖比较逻辑:都使用 areHookInputsEqual 比较依赖项
  2. 相同的缓存机制:都存储在 hook.memoizedState
  3. 相同的决策流程:依赖不变则返回缓存值,变化则更新缓存

唯一的关键区别是:

  • useMemo 需要执行 create() 函数来获取新值
  • useCallback 直接返回传入的 callback 函数本身

3. 从设计意图来看

useMemo 的通用性

useMemo 是一个通用的记忆化工具,可以记忆任何类型的值:

// 记忆计算结果
const expensiveValue = useMemo(() => calculateExpensiveValue(a, b), [a, b]);

// 记忆对象
const config = useMemo(() => ({ timeout: 1000, retries: 3 }), []);

// 记忆数组
const items = useMemo(() => [1, 2, 3, 4], []);

// 记忆函数(这就是 useCallback 的作用!)
const onClick = useMemo(() => () => { /* 函数逻辑 */ }, []);

useCallback 的特化性

useCallback 是专门为记忆函数这个特定场景设计的语法糖:

// 使用 useCallback
const handleClick = useCallback(() => {
  console.log('Clicked!', someValue);
}, [someValue]);

// 等效的 useMemo
const handleClick = useMemo(() => () => {
  console.log('Clicked!', someValue);
}, [someValue]);

4. 从使用场景来看

为什么需要特化的 useCallback?

虽然可以用 useMemo 实现函数记忆,但 useCallback 提供了更好的开发体验:

  1. 更简洁的语法

    // useCallback - 更简洁
    const fn = useCallback(() => {}, [deps]);
    
    // useMemo 等效写法 - 更冗长
    const fn = useMemo(() => () => {}, [deps]);
    
  2. 更清晰的意图表达

    • useCallback 明确表示"我在记忆一个函数"
    • 代码可读性更好,开发者意图更明确
  3. 避免不必要的嵌套

    • useMemo(() => () => {}) 的双箭头函数容易造成混淆
    • useCallback 直接接受要记忆的函数

5. 从实际等价关系来看

// 以下两种写法完全等价:
const memoizedCallback = useCallback(
  () => {
    doSomething(a, b);
  },
  [a, b],
);

const memoizedCallback = useMemo(
  () => () => {
    doSomething(a, b);
  },
  [a, b],
);

这种等价关系清晰地展示了为什么说 useCallbackuseMemo 的特例——它只是 useMemo 在函数记忆这个特定应用场景下的语法糖。

6. 性能考虑

值得注意的是,虽然两者在功能上等价,但在实现上 React 团队还是为 useCallback 做了专门的实现,避免了 useMemo(() => callback, deps) 这种写法中不必要的函数嵌套和创建:

// useMemo 的实现需要多创建一层函数
() => callback // 这一层包装函数每次渲染都会创建

// useCallback 直接存储和返回原始函数
// 稍微更高效一些

但这种性能差异通常可以忽略不计。

7. 什么时候应该使用 useCallback

1. 函数作为 props 传递给优化过的子组件

这是 useCallback 最经典的使用场景:

// 子组件使用了 React.memo 进行优化
const ChildComponent = React.memo(({ onClick, data }) => {
  console.log('子组件渲染');
  return <button onClick={onClick}>点击我: {data}</button>;
});

function ParentComponent() {
  const [count, setCount] = useState(0);
  const [data, setData] = useState('初始数据');
  
  // 使用 useCallback 避免每次渲染都创建新函数
  const handleClick = useCallback(() => {
    console.log('点击处理:', count);
    setData(`更新后的数据 ${count}`);
  }, [count]); // count 变化时才创建新函数
  
  return (
    <div>
      <button onClick={() => setCount(c => c + 1)}>增加计数: {count}</button>
      <ChildComponent onClick={handleClick} data={data} />
    </div>
  );
}

2. 函数作为其他 Hook 的依赖项

当函数被用作 useEffectuseMemo 或其他 Hook 的依赖时:

function Example({ userId }) {
  const [data, setData] = useState(null);
  
  // 使用 useCallback 确保函数引用稳定
  const fetchData = useCallback(async () => {
    const response = await fetch(`/api/user/${userId}`);
    const result = await response.json();
    setData(result);
  }, [userId]); // userId 变化时创建新函数
  
  // useEffect 依赖 fetchData
  useEffect(() => {
    fetchData();
  }, [fetchData]); // 由于 fetchData 引用稳定,effect 不会无限执行
  
  return <div>{data ? data.name : '加载中...'}</div>;
}

3. 函数被传递给事件处理库

当函数需要传递给第三方库,且该库对函数引用敏感时:

function ChartComponent({ data }) {
  const chartRef = useRef();
  
  // 使用 useCallback 确保处理函数引用稳定
  const handleClick = useCallback((event, chartElement) => {
    console.log('图表点击:', chartElement);
    // 处理点击逻辑
  }, []);
  
  useEffect(() => {
    const chart = new ThirdPartyChartLibrary(chartRef.current, {
      onClick: handleClick, // 第三方库可能依赖函数引用
      data: data
    });
    
    return () => chart.destroy();
  }, [data, handleClick]); // handleClick 引用稳定
  
  return <div ref={chartRef} />;
}

8.什么时候不需要使用 useCallback

1. 简单的内联函数

// ❌ 不必要的 useCallback
const handleClick = useCallback(() => {
  console.log('点击');
}, []);

// ✅ 直接使用内联函数即可
<button onClick={() => console.log('点击')}>点击</button>

2. 函数不会传递给子组件

function Component() {
  const [value, setValue] = useState('');
  
  // ❌ 不需要 useCallback,函数只在当前组件使用
  const handleChange = useCallback((e) => {
    setValue(e.target.value);
  }, []);
  
  // ✅ 直接定义函数即可
  const handleChange = (e) => {
    setValue(e.target.value);
  };
  
  return <input value={value} onChange={handleChange} />;
}

3. 函数没有依赖项且性能影响可忽略

// ❌ 过度优化
const handleClick = useCallback(() => {
  window.location.href = '/about';
}, []);

// ✅ 简单情况直接定义函数
const handleClick = () => {
  window.location.href = '/about';
};

9.useCallback 的最佳实践

1. 正确声明依赖项

function Component({ id, name }) {
  const [count, setCount] = useState(0);
  
  // ✅ 正确:包含所有依赖项
  const handleAction = useCallback(() => {
    console.log(`ID: ${id}, Count: ${count}, Name: ${name}`);
    apiCall(id, count);
  }, [id, count, name]); // 所有依赖项都声明
  
  // ❌ 错误:遗漏依赖项
  const badHandleAction = useCallback(() => {
    console.log(`ID: ${id}, Count: ${count}`); // 使用了 id 和 count
  }, [id]); // 遗漏了 count 依赖
  
  return <button onClick={handleAction}>执行操作</button>;
}

2. 与函数式更新结合使用

function Counter() {
  const [count, setCount] = useState(0);
  
  // ✅ 使用函数式更新,避免 count 依赖
  const increment = useCallback(() => {
    setCount(prevCount => prevCount + 1);
  }, []); // 不需要依赖 count
  
  // ❌ 不必要的 count 依赖
  const badIncrement = useCallback(() => {
    setCount(count + 1);
  }, [count]); // 导致 count 变化时创建新函数
  
  return <button onClick={increment}>计数: {count}</button>;
}

3. 与 useMemo 配合优化对象属性

function UserProfile({ user }) {
  // 使用 useCallback 记忆函数
  const onSave = useCallback((userData) => {
    saveUser(user.id, userData);
  }, [user.id]);
  
  // 使用 useMemo 记忆配置对象
  const config = useMemo(() => ({
    title: '编辑用户',
    onSubmit: onSave, // 使用记忆化的函数
    fields: ['name', 'email']
  }), [onSave]); // onSave 引用稳定
  
  return <Form config={config} />;
}

10.常见陷阱与解决方案

1. 依赖项循环

// ❌ 可能导致依赖项循环
const [data, setData] = useState([]);
const fetchData = useCallback(async () => {
  const result = await apiCall();
  setData(result);
}, [data]); // 错误地将 data 作为依赖

// ✅ 使用函数式更新避免循环
const fetchData = useCallback(async () => {
  const result = await apiCall();
  setData(result);
}, []); // 不需要 data 依赖

// ✅ 或者使用 ref 存储最新值
const dataRef = useRef();
dataRef.current = data;

const fetchData = useCallback(async () => {
  const result = await apiCall();
  console.log('当前数据:', dataRef.current); // 通过 ref 访问最新值
  setData(result);
}, []);

2. 过度使用导致性能下降

// ❌ 过度使用 useCallback
const handleClick1 = useCallback(() => {}, []);
const handleClick2 = useCallback(() => {}, []);
const handleClick3 = useCallback(() => {}, []);
// ... 很多 useCallback

// ✅ 合理使用,只在必要时使用
const handleClick1 = () => {};
const handleClick2 = () => {};
const handleClick3 = useCallback(() => {}, []); // 只有这个需要记忆化

3. 与 useEffect 的无限循环

// ❌ 可能导致无限循环
const [data, setData] = useState(null);
const fetchData = use

总结

"useCallback 是 useMemo 的特例" 是基于以下事实:

  1. 概念上的包含关系useCallback 的功能完全可以用 useMemo 实现
  2. 实现上的高度相似:两者共享相同的缓存机制和依赖比较逻辑
  3. 设计上的特化关系useCallback 是专门为函数记忆这个特定场景设计的语法糖
  4. 功能上的等价性useCallback(fn, deps) 等价于 useMemo(() => fn, deps)

useMemo 实现原理

useMemo 实现原理深度解析

useMemo 是 React Hooks 中用于性能优化的重要工具,它的实现涉及到 React 的 Fiber 架构、Hooks 机制和渲染流程。

核心实现机制

useMemo 的核心思想是记忆化(Memoization):在依赖项未变化时返回缓存的值,避免重复计算。

1. 在 React 源码中的基本结构

在 React 源码中,useMemo 的实现大致如下:

function useMemo(create, deps) {
  // 1. 获取当前正在渲染的 Fiber 节点和 Hook 链表
  const dispatcher = resolveDispatcher();
  return dispatcher.useMemo(create, deps);
}

实际的实现位于 ReactFiberHooks.js 中:

function updateMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 2. 获取当前 Hook 对象
  const hook = updateWorkInProgressHook();
  
  // 3. 获取上一次的依赖项和记忆值
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null) {
    if (nextDeps !== null) {
      // 4. 比较依赖项是否变化
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 5. 依赖未变化,返回缓存的值
        return prevState[0];
      }
    }
  }
  
  // 6. 依赖变化或首次渲染,重新计算值
  const nextValue = create();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

Hook 在 Fiber 架构中的存储

要理解 useMemo,需要先了解 Hooks 在 React Fiber 架构中的存储方式:

1. Hook 链表结构

// Fiber 节点中的 Hook 存储
type Fiber = {
  memoizedState: any, // 指向当前 Hook 链表的头部
  // ... 其他属性
};

// Hook 对象结构
type Hook = {
  memoizedState: any,     // 存储记忆的值
  baseState: any,         // 基础状态
  baseQueue: any,         // 基础队列
  queue: any,             // 更新队列
  next: Hook | null,      // 指向下一个 Hook
};

对于 useMemomemoizedState 字段存储的是一个数组 [value, deps]

  • value: 记忆的计算结果
  • deps: 上一次的依赖项数组

2. Hooks 的调用顺序规则

React 依赖 Hook 的调用顺序来正确关联状态:

function MyComponent() {
  const [state] = useState(0);      // Hook 1
  const memoizedValue = useMemo(() => { // Hook 2
    return expensiveCalculation(state);
  }, [state]);
  const [anotherState] = useState(''); // Hook 3
  
  // React 通过调用顺序维护 Hook 链表:
  // Hook1 -> Hook2 -> Hook3
}

依赖比较的实现

useMemo 的核心在于依赖比较,React 使用 areHookInputsEqual 函数:

function areHookInputsEqual(
  nextDeps: Array<mixed>,
  prevDeps: Array<mixed> | null,
): boolean {
  // 处理边界情况
  if (prevDeps === null) {
    return false;
  }
  
  // 比较数组长度
  if (nextDeps.length !== prevDeps.length) {
    console.error(
      'useMemo received a different number of dependencies. ' +
      'Previous: %s, current: %s',
      prevDeps.length,
      nextDeps.length,
    );
    return false;
  }
  
  // 逐个比较依赖项
  for (let i = 0; i < prevDeps.length; i++) {
    // 使用 Object.is 进行严格比较
    if (!objectIs(nextDeps[i], prevDeps[i])) {
      return false;
    }
  }
  
  return true;
}

// Object.is 的 polyfill(处理 NaN 和 ±0 的特殊情况)
function objectIs(a, b) {
  return (
    (a === b && (a !== 0 || 1 / a === 1 / b)) || 
    (a !== a && b !== b) // NaN === NaN
  );
}

完整的渲染流程中的 useMemo

1. 首次渲染(Mount)

function mountMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 1. 创建新的 Hook 对象并添加到链表
  const hook = mountWorkInProgressHook();
  
  // 2. 获取依赖项
  const nextDeps = deps === undefined ? null : deps;
  
  // 3. 执行计算函数
  const nextValue = create();
  
  // 4. 存储值和依赖项
  hook.memoizedState = [nextValue, nextDeps];
  
  return nextValue;
}

2. 更新渲染(Update)

function updateMemo<T>(
  create: () => T,
  deps: Array<mixed> | void | null,
): T {
  // 1. 获取对应的 Hook 对象
  const hook = updateWorkInProgressHook();
  
  // 2. 获取新的依赖项
  const nextDeps = deps === undefined ? null : deps;
  
  // 3. 获取上一次存储的状态
  const prevState = hook.memoizedState;
  
  // 4. 比较依赖项
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        // 依赖未变化,返回缓存的值
        return prevState[0];
      }
    }
  }
  
  // 5. 依赖变化,重新计算
  const nextValue = create();
  hook.memoizedState = [nextValue, nextDeps];
  return nextValue;
}

与 React 渲染周期的整合

useMemo 的执行与 React 的渲染周期密切相关:

  1. Render 阶段useMemo 在组件函数执行期间被调用
  2. 计算时机:计算函数在渲染期间执行(不是副作用阶段)
  3. 缓存策略:缓存的值仅在渲染期间有效
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps,
  renderLanes,
) {
  // 准备渲染环境
  prepareToUseHooks(current, workInProgress, renderLanes);
  
  try {
    // 执行组件函数,useMemo 在此期间被调用
    let nextChildren = Component(nextProps, ref);
    
    // 处理子节点...
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
    
    return workInProgress.child;
  } finally {
    // 清理 Hook 环境
    resetHooksAfterRender();
  }
}

性能考虑与优化细节

1. 记忆化策略的权衡

useMemo 的实现需要在多个方面进行权衡:

// 伪代码:useMemo 的成本效益分析
function shouldUseMemo(create, deps) {
  const calculationCost = estimateCalculationCost(create);
  const comparisonCost = deps.length * SINGLE_COMPARISON_COST;
  const memoryCost = MEMORY_PER_MEMOIZED_VALUE;
  
  // 只有当计算成本 > (比较成本 + 内存成本) 时,使用 useMemo 才有意义
  return calculationCost > (comparisonCost + memoryCost);
}

2. 依赖项数组的特殊处理

// 处理各种依赖项情况
function normalizeDeps(deps) {
  if (deps === undefined || deps === null) {
    // 没有提供依赖项,每次渲染都重新计算
    return null;
  }
  
  if (!Array.isArray(deps)) {
    // 开发环境下警告
    console.error('useMemo expects an array of dependencies.');
    return null;
  }
  
  return deps;
}

3. 开发环境下的额外检查

React 在开发环境下提供了额外的警告和检查:

function useMemo(create, deps) {
  if (__DEV__) {
    // 验证 Hook 调用规则
    validateHookCall();
    
    // 检查依赖项是否是数组
    if (deps !== undefined && deps !== null && !Array.isArray(deps)) {
      console.error(
        'useMemo requires an array of dependencies. Got: %s',
        typeof deps,
      );
    }
    
    // 记录 Hook 的使用以便调试
    recordHook();
  }
  
  // ... 实际实现
}

与 useCallback 的关系

useCallback 实际上是 useMemo 的特例:

function useCallback(callback, deps) {
  return useMemo(() => callback, deps);
}

// 在 React 源码中的实际实现:
function updateCallback(callback, deps) {
  const hook = updateWorkInProgressHook();
  const nextDeps = deps === undefined ? null : deps;
  const prevState = hook.memoizedState;
  
  if (prevState !== null) {
    if (nextDeps !== null) {
      const prevDeps = prevState[1];
      if (areHookInputsEqual(nextDeps, prevDeps)) {
        return prevState[0];
      }
    }
  }
  
  hook.memoizedState = [callback, nextDeps];
  return callback;
}

实际应用中的注意事项

1. 正确使用依赖项

function MyComponent({ items, filter }) {
  // 正确:包含所有依赖项
  const filteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items, filter]); // ✓ 所有依赖项都包含

  // 错误:遗漏依赖项
  const badFilteredItems = useMemo(() => {
    return items.filter(item => item.includes(filter));
  }, [items]); // ✗ 遗漏了 filter 依赖
}

2. 避免过度优化

function MyComponent({ value }) {
  // 不必要的 useMemo:计算很简单
  const simpleValue = useMemo(() => value + 1, [value]); // ✗ 过度优化
  
  // 必要的 useMemo:计算很昂贵
  const expensiveValue = useMemo(() => {
    return expensiveCalculation(value);
  }, [value]); // ✓ 合理的优化
}

总结

useMemo 的实现原理可以概括为:

  1. 基于 Hook 机制:利用 React 的 Hook 链表结构存储记忆的值和依赖项
  2. 依赖比较:使用严格比较(Object.is)检查依赖项是否变化
  3. 记忆化策略:依赖未变化时返回缓存值,变化时重新计算
  4. 渲染期间执行:计算函数在渲染阶段执行,不是副作用
  5. 性能权衡:在计算成本、比较成本和内存使用之间取得平衡
❌