React笔记

95 阅读22分钟

一、React Hook 的使用限制

必须在函数组件或自定义 Hook 的最顶层调用 Hooks,不能放在循环、条件语句或嵌套函数中。

  1. React 通过“调用顺序”来记录 Hook 状态

React 内部使用链表来存储 Hook 状态,在函数组件的 fiber 节点(FiberNode)上,有一个 memoizedState 指针,它指向一个 单向链表,链表的每个节点就是一个 Hook 对象(Hook)。

每个 Hook 节点大致长这样:

type Hook = {
  memoizedState: any,     // 当前的 state 或 effect 的依赖
  baseState: any,         // 初始状态(主要用于 useReducer)
  baseQueue: any,         // 更新队列(useReducer/useState 的 setXXX)
  queue: any,             // 链接 setState 的更新任务
  next: Hook | null       // 指向下一个 Hook(链表结构)
}

fiber.memoizedState 指向链表的头部:

fiber.memoizedState -> Hook1 -> Hook2 -> Hook3 -> null

React 渲染组件时,每调用一次 Hook,就沿着链表往下走一步。

(1)只能在函数组件顶层调用:因为React依赖调用顺序来匹配链表中的节点

(2)不能在条件语句、循环或嵌套函数中调用:这会破坏调用顺序,导致链表节点匹配错误

  1. 如果放在条件语句中,会破坏顺序
function MyComponent({ flag }) {
  if (flag) {
    const [count, setCount] = useState(0); // ❌
  }
  const [name, setName] = useState("A");
}
  • flag = true 时,调用顺序是:

    (1). useState(0)count

    (2). useState("A")name

  • flag = false 时,调用顺序变成:

    useState("A") → React 仍然认为这是第一个 Hook

结果:name 的状态会错乱到 count 的位置,React 内部的 hooks 链表错位,渲染逻辑全乱。

  1. 如果放在循环中,同样会错乱
function MyComponent({ list }) {
  for (let i = 0; i < list.length; i++) {
    const [value, setValue] = useState(i); // ❌
  }
}
  • 如果第一次渲染 list.length = 3,React 内部会创建 3 个 useState
  • 下一次渲染 list.length = 5,React 需要创建 5 个 useState
  • 顺序和数量都变了,React 内部的状态链条直接乱掉。
  1. 如果放在嵌套函数里,也会破坏一致性
function MyComponent() {
  function inner() {
    const [value, setValue] = useState(0); // ❌
  }
  inner();
}
  • React 在进入组件渲染时,会按照顺序一层层执行 Hook。
  • 但这里 useState 是在运行时的 inner() 才调用的,React 无法在组件“顶层”预测到底会不会执行。
  • 这会导致 Hook 的注册和执行不稳定,破坏调用顺序。
  1. 为什么是链表?
  • 链表便于动态扩展:不同组件可能有不同数量、不同类型的 Hook。
  • 插入/删除 Hook 方便,比如热更新或 fiber 切换时,可以很快重连。
  • 避免数组重新分配、下标错位的问题。

二、React Diff 算法详解

1. 概述

React的Diff算法是Virtual DOM机制的核心部分,负责比较新旧Virtual DOM树的差异,并计算出最小的DOM操作来更新真实DOM。这个算法的设计目标是在保证正确性的前提下,尽可能提高性能。

2. 传统Diff算法的复杂度问题

传统的树形结构diff算法的时间复杂度为O(n³),其中n是树中节点的数量。这是因为:

  • 需要遍历两棵树的每个节点 (O(n²))
  • 对每个节点需要计算编辑距离 (O(n))

对于包含1000个元素的页面,这意味着需要进行10亿次比较,这在实际应用中是无法接受的。

3. React Diff算法的三大策略

React基于以下三个假设,将O(n³)复杂度转换为O(n):

Tree Diff (层级遍历)
    ↓
Component Diff (组件类型判断)
    ↓  
Element Diff (具体元素比较)

3.1 策略一:Tree Diff - 分层比较

假设:Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计

  • 只会对相同层级的节点进行比较
  • 如果某个节点不存在了,该节点及其子节点会被完全删除,不会用于进一步比较
  • 如果发现跨层级移动,不会进行移动操作,而是删除重建
旧树:        新树:
  A            A
 / \          / \
B   C        D   C
   /            / \
  D            B   E

在上面的例子中,节点D从C的子节点变成了A的子节点。React会:

  1. 删除C下的D
  2. 在A下创建新的D及其子树

3.2 策略二:Component Diff - 组件比较

假设:拥有相同类的两个组件会生成相似的树形结构,拥有不同类的两个组件会生成不同的树形结构

React对组件的比较策略:

  1. 同类型组件:按照原策略继续比较Virtual DOM树
  2. 不同类型组件:判定为dirty component,替换整个组件下的所有子节点
  3. 同类型组件,Virtual DOM没有变化:React允许用户通过shouldComponentUpdate()React.memo()来判断该组件是否需要diff
// 不同类型组件的例子
// 旧组件
<div>
  <ComponentA />
</div>
​
// 新组件
<div>
  <ComponentB />
</div>
​
// React会删除ComponentA,重新创建ComponentB

3.3 策略三:Element Diff - 元素比较

假设:对于同一层级的一组子节点,可以通过唯一的key来区分

对于同一层级的子节点,React提供了三种节点操作:

  • INSERT_MARKUP:插入新节点
  • MOVE_EXISTING:移动现有节点
  • REMOVE_NODE:删除节点
Key的重要性
// 没有key的情况
<ul>
  <li>张三</li>
  <li>李四</li>
  <li>王五</li>
</ul>
​
// 在开头插入新元素
<ul>
  <li>赵六</li>  {/* 会被认为是修改:张三->赵六 */}
  <li>张三</li>  {/* 会被认为是修改:李四->张三 */}
  <li>李四</li>  {/* 会被认为是修改:王五->李四 */}
  <li>王五</li>  {/* 会被认为是新插入 */}
</ul>
// 有key的情况
<ul>
  <li key="zhang">张三</li>
  <li key="li">李四</li>
  <li key="wang">王五</li>
</ul>
​
// 在开头插入新元素
<ul>
  <li key="zhao">赵六</li>  {/* 新插入 */}
  <li key="zhang">张三</li> {/* 位置移动,但元素复用 */}
  <li key="li">李四</li>    {/* 位置移动,但元素复用 */}
  <li key="wang">王五</li>  {/* 位置移动,但元素复用 */}
</ul>

4. Diff算法的具体执行流程

4.1 树的遍历方式

React采用深度优先遍历的方式对比两棵Virtual DOM树:

    A
   / \
  B   C
 /   / \
D   E   F

遍历顺序:A -> B -> D -> C -> E -> F

4.2 节点比较流程

// 伪代码
function diff(oldNode, newNode) {
  // 1. 如果新节点不存在,删除旧节点
  if (newNode === null) {
    return { type: 'REMOVE', oldNode };
  }
  
  // 2. 如果旧节点不存在,插入新节点
  if (oldNode === null) {
    return { type: 'INSERT', newNode };
  }
  
  // 3. 如果节点类型不同,替换节点
  if (oldNode.type !== newNode.type) {
    return { type: 'REPLACE', oldNode, newNode };
  }
  
  // 4. 如果是文本节点且内容不同,更新文本
  if (isTextNode(oldNode) && oldNode.text !== newNode.text) {
    return { type: 'TEXT', newText: newNode.text };
  }
  
  // 5. 如果是元素节点,比较属性和子节点
  if (isElementNode(oldNode)) {
    const propsDiff = diffProps(oldNode.props, newNode.props);
    const childrenDiff = diffChildren(oldNode.children, newNode.children);
    
    return {
      type: 'UPDATE',
      propsDiff,
      childrenDiff
    };
  }
}

4.3 子节点的Diff算法

对于子节点数组的比较,React使用了一个优化的算法:

(1)第一阶段:从左到右遍历

  • 比较新老节点数组从头开始的相同key节点
  • 如果key相同,更新节点内容,继续遍历
  • 如果key不同或遍历到数组末尾,停止遍历

(2)第二阶段:从右到左遍历

  • 比较新老节点数组从尾部开始的相同key节点
  • 如果key相同,更新节点内容,继续遍历
  • 如果key不同或遍历到数组开头,停止遍历

(3)第三阶段:处理简单情况

  • 如果老节点遍历完:批量插入剩余的新节点
  • 如果新节点遍历完:批量删除剩余的老节点
  • 如果都有剩余:进入第四阶段的复杂处理

(4)第四阶段:处理复杂的移动、新增、删除

  1. 建立映射表:将剩余老节点的key和索引建立Map映射

  2. 标记操作类型:遍历剩余新节点,在Map中查找对应老节点

    • 找到:标记为更新或移动
    • 未找到:标记为新增
  3. 检测移动:通过老节点索引的递增性判断是否需要移动

  4. 优化移动:使用最长递增子序列算法计算最少的移动操作

  5. 执行操作:从后往前执行插入、移动、删除操作

这种设计的核心优化点:

  • 双端比较快速处理头尾相同的节点
  • 最长递增子序列最小化DOM移动次数
  • Map查找将查找复杂度从O(n)降为O(1)
  • 从后往前操作避免DOM位置变化的影响
// 子节点Diff伪代码
function diffChildren(oldChildren, newChildren) {
  let oldStartIdx = 0;
  let newStartIdx = 0;
  let oldEndIdx = oldChildren.length - 1;
  let newEndIdx = newChildren.length - 1;
  
  // 第一轮遍历:从左到右比较
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    const oldNode = oldChildren[oldStartIdx];
    const newNode = newChildren[newStartIdx];
    
    if (oldNode.key === newNode.key) {
      // key相同,更新节点
      diff(oldNode, newNode);
      oldStartIdx++;
      newStartIdx++;
    } else {
      // key不同,跳出第一轮遍历
      break;
    }
  }
  
  // 第二轮遍历:从右到左比较
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    const oldNode = oldChildren[oldEndIdx];
    const newNode = newChildren[newEndIdx];
    
    if (oldNode.key === newNode.key) {
      // key相同,更新节点
      diff(oldNode, newNode);
      oldEndIdx--;
      newEndIdx--;
    } else {
      // key不同,跳出第二轮遍历
      break;
    }
  }
  
  // 第三步:处理剩余节点
  if (oldStartIdx > oldEndIdx) {
    // 老节点已经遍历完,剩余的新节点需要插入
    const before = newChildren[newEndIdx + 1];
    for (let i = newStartIdx; i <= newEndIdx; i++) {
      insertNode(newChildren[i], before);
    }
  } else if (newStartIdx > newEndIdx) {
    // 新节点已经遍历完,剩余的老节点需要删除
    for (let i = oldStartIdx; i <= oldEndIdx; i++) {
      removeNode(oldChildren[i]);
    }
  } else {
    // 都有剩余节点,需要处理移动、新增、删除的复杂情况
    handleMovingNodes(oldChildren, newChildren, oldStartIdx, newStartIdx, oldEndIdx, newEndIdx);
  }
}
​
// 处理复杂移动逻辑的详细实现
function handleMovingNodes(oldChildren, newChildren, oldStartIdx, newStartIdx, oldEndIdx, newEndIdx) {
  // 第一步:创建剩余老节点的key映射表
  const oldKeyToIdx = new Map();
  for (let i = oldStartIdx; i <= oldEndIdx; i++) {
    const key = oldChildren[i].key;
    if (key != null) {
      oldKeyToIdx.set(key, i);
    }
  }
  
  // 第二步:遍历剩余新节点,标记移动和新增
  const toBePatched = newEndIdx - newStartIdx + 1; // 需要处理的新节点数量
  const newIndexToOldIndexMap = new Array(toBePatched).fill(-1); // -1表示新增节点
  let moved = false; // 是否需要移动
  let maxNewIndexSoFar = 0; // 已遍历节点在老数组中的最大索引
  let patched = 0; // 已处理的节点数量
  
  // 遍历剩余的新节点
  for (let newIdx = newStartIdx; newIdx <= newEndIdx; newIdx++) {
    const newChild = newChildren[newIdx];
    
    if (patched >= toBePatched) {
      // 新节点已经处理完,删除多余的老节点
      removeNode(newChild);
      continue;
    }
    
    let oldIdx;
    if (newChild.key != null) {
      // 有key,在映射表中查找对应的老节点
      oldIdx = oldKeyToIdx.get(newChild.key);
    } else {
      // 没有key,线性搜索匹配的节点(性能较差)
      for (let i = oldStartIdx; i <= oldEndIdx; i++) {
        if (oldChildren[i].key == null && 
            isSameNodeType(oldChildren[i], newChild)) {
          oldIdx = i;
          break;
        }
      }
    }
    
    if (oldIdx === undefined) {
      // 在老节点中没找到,说明是新增节点
      insertNode(newChild);
    } else {
      // 找到了对应的老节点
      newIndexToOldIndexMap[newIdx - newStartIdx] = oldIdx;
      
      if (oldIdx >= maxNewIndexSoFar) {
        // 老节点索引递增,不需要移动
        maxNewIndexSoFar = oldIdx;
      } else {
        // 老节点索引不是递增的,需要移动
        moved = true;
      }
      
      // 更新节点内容
      diff(oldChildren[oldIdx], newChild);
      patched++;
    }
  }
  
  // 第三步:生成移动操作
  if (moved) {
    // 计算最长递增子序列,这些节点不需要移动
    const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap);
    let j = increasingNewIndexSequence.length - 1;
    
    // 从后往前处理,避免位置变化影响
    for (let i = toBePatched - 1; i >= 0; i--) {
      const nextIndex = newStartIdx + i;
      const nextChild = newChildren[nextIndex];
      const anchor = nextIndex + 1 < newChildren.length 
        ? newChildren[nextIndex + 1] 
        : null;
      
      if (newIndexToOldIndexMap[i] === -1) {
        // 新增节点
        insertNode(nextChild, anchor);
      } else if (j < 0 || i !== increasingNewIndexSequence[j]) {
        // 需要移动的节点
        moveNode(nextChild, anchor);
      } else {
        // 在最长递增子序列中,不需要移动
        j--;
      }
    }
  }
}
​
// 计算最长递增子序列(用于优化移动操作)
function getSequence(arr) {
  const n = arr.length;
  const result = [0];
  const p = new Array(n);
  
  for (let i = 1; i < n; i++) {
    if (arr[i] === -1) continue;
    
    const last = result[result.length - 1];
    if (arr[i] > arr[last]) {
      p[i] = last;
      result.push(i);
    } else {
      // 二分查找
      let left = 0, right = result.length - 1;
      while (left < right) {
        const mid = Math.floor((left + right) / 2);
        if (arr[result[mid]] < arr[i]) {
          left = mid + 1;
        } else {
          right = mid;
        }
      }
      
      if (arr[i] < arr[result[left]]) {
        if (left > 0) p[i] = result[left - 1];
        result[left] = i;
      }
    }
  }
  
  // 回溯构建序列
  let i = result.length;
  let u = result[i - 1];
  while (i-- > 0) {
    result[i] = u;
    u = p[u];
  }
  
  return result;
}
​
// 辅助函数
function isSameNodeType(a, b) {
  return a.type === b.type;
}
​
function insertNode(node, before = null) {
  // 实际DOM插入操作
  console.log(`插入节点: ${node.key}`, before ? `在${before.key}之前` : '在末尾');
}
​
function removeNode(node) {
  // 实际DOM删除操作  
  console.log(`删除节点: ${node.key}`);
}
​
function moveNode(node, before = null) {
  // 实际DOM移动操作
  console.log(`移动节点: ${node.key}`, before ? `到${before.key}之前` : '到末尾');
}

5. React 18中的Diff优化

React 18引入了并发特性,对Diff算法也进行了优化:

5.1 可中断的Diff

  • Diff过程可以被中断,让出主线程给更重要的任务
  • 通过时间切片(Time Slicing)机制,防止长时间的Diff阻塞页面

5.2 优先级调度

  • 不同的更新有不同的优先级
  • 高优先级的更新可以中断低优先级的Diff过程

5.3 Lanes模型

  • 使用Lanes模型来管理不同优先级的更新
  • 更细粒度的批处理和调度
// React 18中的优先级示例
function updateWithPriority(priority, update) {
  const currentLanes = getCurrentLanes();
  const updateLane = priority === 'urgent' ? SyncLane : TransitionLane;
  
  if (updateLane > currentLanes) {
    // 中断当前diff,处理高优先级更新
    interruptCurrentWork();
    scheduleWork(updateLane, update);
  } else {
    // 加入当前批次
    addToBatch(update);
  }
}

6. 性能优化建议

6.1 正确使用key

// ❌ 不要使用数组索引作为key
{items.map((item, index) => 
  <Item key={index} data={item} />
)}
​
// ✅ 使用稳定、唯一的标识符
{items.map(item => 
  <Item key={item.id} data={item} />
)}

6.2 避免不必要的组件重新渲染

// 使用React.memo
const MyComponent = React.memo(function MyComponent({ name }) {
  return <div>{name}</div>;
});
​
// 使用useMemo和useCallback
function Parent() {
  const expensiveValue = useMemo(() => computeExpensiveValue(), [dep]);
  const stableCallback = useCallback(() => doSomething(), [dep]);
  
  return <Child value={expensiveValue} onClick={stableCallback} />;
}

6.3 合理的组件拆分

// ❌ 过大的组件会导致不必要的diff
function LargeComponent() {
  return (
    <div>
      <Header />
      <ExpensiveList items={items} />
      <Footer />
    </div>
  );
}
​
// ✅ 将不变的部分抽取为独立组件
const Header = React.memo(function Header() {
  return <header>...</header>;
});
​
const Footer = React.memo(function Footer() {
  return <footer>...</footer>;
});

7. 调试和性能监控

7.1 React DevTools

  • 使用Profiler面板查看组件渲染时间
  • 查看组件重新渲染的原因

7.2 性能监控代码

// 测量组件渲染时间
function MyComponent() {
  const renderStartTime = performance.now();
  
  useEffect(() => {
    const renderEndTime = performance.now();
    console.log(`Render time: ${renderEndTime - renderStartTime}ms`);
  });
  
  return <div>...</div>;
}

8. 总结

React的Diff算法通过三大策略将复杂度从O(n³)降低到O(n):

  1. Tree Diff:只比较同层级节点
  2. Component Diff:同类型组件生成相似树结构的假设
  3. Element Diff:通过key标识同层级节点

这种设计在实际的Web应用场景下表现优异,但也需要开发者理解其原理,合理使用key,避免跨层级移动,并通过适当的组件设计来配合算法发挥最佳性能。

React 18的并发特性进一步增强了Diff算法的能力,通过可中断的渲染和优先级调度,让应用能够更好地响应用户交互,提供更流畅的用户体验。

三、React Fiber架构详解

1. 什么是Fiber

Fiber是React 16引入的全新协调(reconciliation)引擎,它是React核心算法的完全重写。Fiber的设计目标是增强React在动画、布局、手势等场景下的适应性,实现增量式渲染,将渲染工作分解成多个片段,并将其分散到多个帧中执行。

2. 为什么需要Fiber

2.1 旧版React的问题

在React 15及之前的版本中,协调过程是同步且递归的,存在以下问题:

  • 阻塞渲染:一旦开始更新,就必须完成整个组件树的遍历
  • 无法中断:长时间运行的任务会阻塞主线程
  • 用户体验差:导致页面卡顿,特别是在复杂应用中
  • 优先级缺失:无法区分紧急更新和普通更新

2.2 Fiber解决的核心问题

  • 可中断性:将大任务分解成小任务,可以被中断和恢复
  • 优先级调度:不同类型的更新具有不同的优先级
  • 并发渲染:支持时间切片,充分利用浏览器的空闲时间
  • 错误边界:更好的错误处理机制

3. Fiber的核心概念

3.1 什么是Fiber节点

Fiber节点是一个JavaScript对象,代表了组件、DOM节点或其他React元素的工作单元。每个Fiber节点包含以下关键信息:

{
  // 节点类型信息
  tag: WorkTag,                 // 节点类型(函数组件、类组件、DOM节点等)
  type: any,                   // 组件类型或DOM标签
  key: null | string,          // React key
  
  // 节点关系
  child: Fiber | null,         // 第一个子节点
  sibling: Fiber | null,       // 下一个兄弟节点
  return: Fiber | null,        // 父节点
  index: number,               // 在兄弟节点中的索引
  
  // 状态和属性
  pendingProps: any,           // 新的props
  memoizedProps: any,          // 上一次渲染的props
  memoizedState: any,          // 上一次渲染的state
  updateQueue: UpdateQueue | null, // 更新队列
  
  // 副作用
  flags: Flags,                // 副作用标记
  subtreeFlags: Flags,         // 子树副作用标记
  deletions: Array<Fiber> | null, // 需要删除的子节点
  
  // 调度相关
  lanes: Lanes,                // 优先级信息
  childLanes: Lanes,           // 子节点的优先级
  
  // 双缓存
  alternate: Fiber | null,     // 对应的另一个Fiber节点
}

3.2 Fiber树结构

Fiber采用链表结构来表示组件树:

        App
       /   \
    Header  Main
   /   |     \
 Logo Nav   Content

在Fiber中表示为:

  • 每个节点只有一个child指针指向第一个子节点
  • 兄弟节点通过sibling指针连接
  • 所有子节点都有return指针指向父节点

4. 双缓存机制

4.1 工作原理

React Fiber使用双缓存技术,维护两个Fiber树:

  • current树:当前显示在屏幕上的Fiber树
  • workInProgress树:正在构建的新Fiber树

4.2 构建过程

  1. 创建workInProgress树:基于current树创建新的工作树
  2. 协调过程:在workInProgress树上进行diff和更新
  3. 提交阶段:将workInProgress树应用到DOM
  4. 树切换:workInProgress树变成新的current树
// 双缓存节点关系
currentFiber.alternate = workInProgressFiber;
workInProgressFiber.alternate = currentFiber;

5. Fiber的工作循环

5.1 渲染阶段(Render Phase)

这个阶段是可中断的,主要工作包括:

function workLoopConcurrent() {
  // 当有工作要做且没有被中断时继续工作
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  
  // 处理当前节点
  let next = beginWork(current, unitOfWork, renderLanes);
  
  if (next === null) {
    // 如果没有子节点,完成当前节点
    completeUnitOfWork(unitOfWork);
  } else {
    // 继续处理子节点
    workInProgress = next;
  }
}

5.2 提交阶段(Commit Phase)

这个阶段是不可中断的,分为三个子阶段:

  1. before mutation:执行DOM操作前
  2. mutation:执行DOM操作
  3. layout:DOM操作后,执行副作用
function commitRoot(root) {
  // 阶段1:before mutation
  commitBeforeMutationEffects(root, finishedWork);
  
  // 阶段2:mutation
  commitMutationEffects(root, finishedWork);
  
  // 切换current指针
  root.current = finishedWork;
  
  // 阶段3:layout
  commitLayoutEffects(finishedWork, root);
}

6. 优先级调度

6.1 Lane模型

Fiber使用Lane模型来管理优先级:

// 不同类型的Lane优先级
const SyncLane = 0b0000000000000000000000000000001;
const InputContinuousLane = 0b0000000000000000000000000000100;
const DefaultLane = 0b0000000000000000000000000010000;
const TransitionLane = 0b0000000000000000000001000000000;
const IdleLane = 0b0100000000000000000000000000000;

6.2 调度器(Scheduler)

React使用Scheduler来实现时间切片:

// 根据优先级调度任务
function scheduleUpdateOnFiber(fiber, lane) {
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  
  if (lane === SyncLane) {
    // 同步更新
    performSyncWorkOnRoot(root);
  } else {
    // 并发更新
    scheduleCallback(NormalPriority, performConcurrentWorkOnRoot.bind(null, root));
  }
}

7. Hooks与Fiber

7.1 Hooks的实现

Hooks依赖于Fiber节点的memoizedState字段:

// Hook对象结构
const hook = {
  memoizedState: null,  // 上次渲染的状态
  baseState: null,      // 基础状态
  baseQueue: null,      // 基础更新队列
  queue: null,          // 更新队列
  next: null,           // 下一个Hook
};

7.2 useState的工作流程

function useState(initialState) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

function mountState(initialState) {
  const hook = mountWorkInProgressHook();
  hook.memoizedState = hook.baseState = initialState;
  
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
  });
  
  const dispatch = (queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber, queue));
  return [hook.memoizedState, dispatch];
}

8. 错误边界

8.1 错误处理机制

Fiber提供了更好的错误边界处理:

function throwException(root, returnFiber, sourceFiber, value, rootRenderLanes) {
  sourceFiber.flags |= Incomplete;
  
  // 寻找最近的错误边界
  let workInProgress = returnFiber;
  do {
    switch (workInProgress.tag) {
      case ClassComponent:
        if (workInProgress.type.getDerivedStateFromError !== undefined ||
            workInProgress.stateNode.componentDidCatch !== undefined) {
          // 找到错误边界
          return throwException(root, workInProgress, sourceFiber, value, rootRenderLanes);
        }
        break;
    }
    workInProgress = workInProgress.return;
  } while (workInProgress !== null);
}

9. 性能优化

9.1 bailout策略

Fiber通过多种策略来跳过不必要的工作:

function bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) {
  // 检查子节点是否需要更新
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    // 子节点不需要更新,直接跳过
    return null;
  }
  
  // 克隆子节点继续工作
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

9.2 时间切片

// 检查是否应该让出控制权
function shouldYield() {
  return getCurrentTime() >= deadline;
}

// Scheduler中的时间切片实现
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

10. Fiber与并发特性

10.1 Concurrent Mode

Fiber为React的并发模式奠定了基础:

  • 可中断渲染:长任务可以被分割成小片段
  • 优先级调度:高优先级任务可以打断低优先级任务
  • 时间切片:将渲染工作分散到多个帧中

10.2 Suspense

Suspense功能依赖于Fiber的错误边界机制:

function throwSuspense(suspense) {
  // 抛出特殊的Promise异常
  throw suspense;
}

// 在错误边界中捕获Suspense
if (typeof value === 'object' && value !== null && typeof value.then === 'function') {
  // 这是一个Suspense Promise
  const suspenseBoundary = findSuspenseBoundary(returnFiber);
  if (suspenseBoundary !== null) {
    suspenseBoundary.flags |= ShouldCapture;
  }
}

11. 总结

React Fiber架构的核心优势:

  1. 可中断性:解决了长任务阻塞主线程的问题
  2. 优先级调度:让重要的更新能够优先执行
  3. 错误边界:提供了更好的错误恢复机制
  4. 并发渲染:为Future的并发特性奠定了基础
  5. 更好的用户体验:通过时间切片避免页面卡顿

Fiber架构的引入使React从一个简单的UI库演进为一个强大的用户界面开发平台,为复杂应用的开发提供了强有力的支持。理解Fiber架构对于深入掌握React的工作原理和性能优化具有重要意义。

四、React Hooks

1. useState

用于在函数组件中添加状态管理功能。它让函数组件能够拥有自己的内部状态。

(1)基本语法

const [state, setState] = useState(initialValue);
  • state: 当前状态值
  • setState: 更新状态的函数
  • initialValue: 状态的初始值

(2)重要特性

  • 状态更新是异步的
function AsyncExample() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    console.log('点击前:', count); // 0
    setCount(count + 1);
    console.log('点击后:', count); // 仍然是 0,因为状态更新是异步的
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}
  • 函数式更新:当新状态依赖于前一个状态时,需使用函数式更新:
function Counter() {
  const [count, setCount] = useState(0);
  
  const incrementTwice = () => {
    // 错误方式:可能不会按预期工作
    // setCount(count + 1);
    // setCount(count + 1);
    
    // 正确方式:使用函数式更新
    setCount(prevCount => prevCount + 1);
    setCount(prevCount => prevCount + 1);
  };
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={incrementTwice}>增加2</button>
    </div>
  );
}
  • React 会对多个状态更新进行批处理:
function BatchingExample() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  
  const handleClick = () => {
    // 这些更新会被批处理,只触发一次重新渲染
    setCount(c => c + 1);
    setFlag(f => !f);
  };
  
  console.log('渲染'); // 只会打印一次
  
  return (
    <div>
      <p>Count: {count}</p>
      <p>Flag: {flag.toString()}</p>
      <button onClick={handleClick}>更新</button>
    </div>
  );
}

(3)简单实现

// 存放当前组件中所有 hook 的状态值
// 比如 useState(0)、useState('hi') 都会在这里按顺序存储
let hookStates = [];

// 当前正在执行的 hook 的索引(位置)
// 每次执行一个 useState,就会往后移动一位
let hookIndex = 0;

// 模拟 React 的 useState 实现
function useState(initialValue) {
  // 记录当前 useState 在 hooks 数组中的位置
  const currentIndex = hookIndex;
  
  // 初始化阶段(第一次渲染):
  // 如果当前索引没有值,则用 initialValue 初始化
  // 否则说明已经渲染过,直接复用旧值
  hookStates[currentIndex] = hookStates[currentIndex] ?? initialValue;
  
  // 定义更新函数(相当于 setCount)
  const setState = (newValue) => {
    // 支持两种形式:
    // 1. 直接传值:setCount(2)
    // 2. 传函数:setCount(prev => prev + 1)
    hookStates[currentIndex] = 
      typeof newValue === 'function'
        ? newValue(hookStates[currentIndex]) // 函数式更新
        : newValue;                           // 直接赋值

    // 状态更新后重新渲染组件
    render();
  };
  
  // 每调用一个 useState,索引加 1,保证下一个 Hook 能存到不同位置
  hookIndex++;
  
  // 返回当前状态值和更新函数
  return [hookStates[currentIndex], setState];
}

// 模拟组件重新渲染
function render() {
  // 每次重新渲染前,将索引重置为 0
  // 因为组件会重新从头执行(React 每次 render 都是函数重跑)
  hookIndex = 0;

  // 执行函数组件本身
  App();
}

2. useEffect

用于处理副作用,包括:请求数据、设置订阅、操作 DOM、定时器、清理资源。

(1)基本语法

useEffect(() => {
  console.log('组件挂载或更新');
  return () => {
    console.log('组件卸载或清理');
  };
}, [count]); // 依赖项改变时才执行

第二个参数是依赖数组: 空数组 []时,只在挂载时执行一次;有依赖项时,依赖项变化时执行;无依赖数组时,每次渲染都执行。

(2)在return 中,可以取消请求、清理定时器、移除事件监听器

// 取消请求
function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  
  useEffect(() => {
    if (!query) {
      setResults([]);
      return;
    }
    
    const abortController = new AbortController();
    
    const searchData = async () => {
      try {
        setLoading(true);
        const response = await fetch(`/api/search?q=${query}`, {
          signal: abortController.signal
        });
        const data = await response.json();
        setResults(data);
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('搜索失败:', error);
        }
      } finally {
        setLoading(false);
      }
    };
    
    searchData();
    
    // 清理函数:取消请求
    return () => {
      abortController.abort();
    };
  }, [query]);
  
  return (
    <div>
      {loading && <div>搜索中...</div>}
      <ul>
        {results.map(item => (
          <li key={item.id}>{item.title}</li>
        ))}
      </ul>
    </div>
  );
}

3. useContext

用于在组件树中跨层级传递数据,避免了逐层传递 props的问题。能够在不通过 props 的情况下,将数据传递给深层嵌套的组件。

  • Context: 上下文对象,用于存储共享数据
  • Provider: 提供者组件,用于提供数据
  • Consumer: 消费者组件,用于使用数据
import React, { createContext, useContext, useState } from 'react';
​
// 1. 创建 Context
const ThemeContext = createContext();
​
// 2. 创建 Provider 组件
function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');
  
  const toggleTheme = () => {
    setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
  };
  
  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
​
// 3. 在子组件中使用 Context
function Header() {
  const { theme, toggleTheme } = useContext(ThemeContext);
  
  return (
    <header style={{ 
      backgroundColor: theme === 'light' ? '#fff' : '#333',
      color: theme === 'light' ? '#333' : '#fff'
    }}>
      <h1>我的应用</h1>
      <button onClick={toggleTheme}>
        切换到 {theme === 'light' ? '深色' : '浅色'} 模式
      </button>
    </header>
  );
}
​
function Content() {
  const { theme } = useContext(ThemeContext);
  
  return (
    <main style={{ 
      backgroundColor: theme === 'light' ? '#f5f5f5' : '#222',
      color: theme === 'light' ? '#333' : '#fff',
      padding: '20px'
    }}>
      <p>当前主题: {theme}</p>
    </main>
  );
}
​
// 4. 在应用中使用
function App() {
  return (
    <ThemeProvider>
      <Header />
      <Content />
    </ThemeProvider>
  );
}

4. useRef

用于获取 DOM 节点或保存可变值,useRef 返回一个对象,该对象有一个 current 属性,可以通过修改 current 来存储任何值。与 useState 不同的是,修改 useRef 的值不会触发组件重新渲染。

function TextInput() {
  const inputRef = useRef(null);
  
  const focusInput = () => {
    inputRef.current.focus();
  };
  
  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

5. useMemo

可以缓存计算结果,只有在依赖项发生变化时才会重新计算。

(1)基本语法

import { useMemo } from 'react';
​
const memoizedValue = useMemo(() => {
  return expensiveCalculation(a, b);
}, [a, b]);

useMemo 接收两个参数:

  • 计算函数:返回需要缓存的值
  • 依赖数组:当数组中的值发生变化时,才会重新执行计算函数

(2) 深比较函数

function deepEqual(a, b) {
  // 1️⃣ 如果两者是同一个引用或基本类型值相等
  if (a === b) return true;

  // 2️⃣ 处理 NaN(因为 NaN !== NaN,但语义上应视为相等)
  if (Number.isNaN(a) && Number.isNaN(b)) return true;

  // 3️⃣ 如果类型不同,或其中一个是 null(typeof null === 'object')
  if (typeof a !== typeof b || a === null || b === null) return false;

  // 4️⃣ 如果是基本类型(非对象、非数组),直接比较
  if (typeof a !== "object") return a === b;

  // 5️⃣ 处理 Date 对象(用时间戳比较)
  if (a instanceof Date && b instanceof Date) {
    return a.getTime() === b.getTime();
  }

  // 6️⃣ 处理正则对象(用源和标志位比较)
  if (a instanceof RegExp && b instanceof RegExp) {
    return a.source === b.source && a.flags === b.flags;
  }

  // 7️⃣ 如果是数组
  if (Array.isArray(a) && Array.isArray(b)) {
    // 长度不同,直接 false
    if (a.length !== b.length) return false;
    // 逐项递归比较
    for (let i = 0; i < a.length; i++) {
      if (!deepEqual(a[i], b[i])) return false;
    }
    return true;
  }

  // 8️⃣ 如果是普通对象(非数组)
  const keysA = Object.keys(a);
  const keysB = Object.keys(b);

  // 键数量不同,直接不相等
  if (keysA.length !== keysB.length) return false;

  // 检查每个 key 的存在性和对应值
  for (const key of keysA) {
    // b 中没有相同 key,直接 false
    if (!Object.prototype.hasOwnProperty.call(b, key)) return false;

    // 递归比较子属性
    if (!deepEqual(a[key], b[key])) return false;
  }

  // 9️⃣ 所有 key 都通过,返回 true
  return true;
}

(3)深比较useMemo

function useDeepMemo(factory, deps) {
  // 用于保存上一次的依赖数组
  const lastDepsRef = useRef();
  // 用于保存上一次计算得到的结果
  const lastValueRef = useRef();

  // 🧠 如果已经有缓存过依赖(即不是第一次执行)
  // 且当前依赖与上次依赖“深比较相等”,说明依赖没有变化
  if (lastDepsRef.current && deepEqual(lastDepsRef.current, deps)) {
    // ✅ 直接返回上一次计算的结果,避免重复计算
    return lastValueRef.current;
  }

  // 🚀 如果依赖发生变化(或者是第一次执行),重新调用 factory() 计算结果
  const newValue = factory();

  // 📝 把当前依赖和计算结果缓存起来
  // 以便下次渲染时比较依赖变化,决定是否复用结果
  lastDepsRef.current = deps;
  lastValueRef.current = newValue;

  // 返回最新计算结果
  return newValue;
}

6. useCallback

用于缓存函数,返回一个记忆化的回调函数,只有在依赖项发生变化时才会重新创建函数。

import { useCallback } from 'react';
​
const memoizedCallback = useCallback(() => {
  // 函数逻辑
}, [dependency1, dependency2]);

useCallback 接收两个参数:

  • 回调函数:需要缓存的函数
  • 依赖数组:当数组中的值发生变化时,才会重新创建函数

7. useReducer

useState 的替代方案,特别适用于复杂的状态逻辑管理。

import { useReducer } from 'react';
const [state, dispatch] = useReducer(reducer, initialState);
function Counter() {
  const initialState = { count: 0 };
  
  function reducer(state, action) {
    switch (action.type) {
      case 'increment':
        return { count: state.count + 1 };
      case 'decrement':
        return { count: state.count - 1 };
      case 'reset':
        return initialState;
      default:
        throw new Error();
    }
  }
  
  const [state, dispatch] = useReducer(reducer, initialState);
  
  return (
    <div>
      <p>计数: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'reset' })}>重置</button>
    </div>
  );
}