阶段 2:框架原理 - 从"会用"到"懂原理"

0 阅读6分钟

阶段 2:框架原理 - 从"会用"到"懂原理"

你提到的这几个点确实是区分"熟练工"和"懂原理的人"的分水岭。我来逐一拆解。


一、React 为什么快?—— 先破题

这个问题本身就是个陷阱题。正确的回答思路:

React 并不总是快,它的优势在于通过巧妙的设计,在保证开发体验的前提下,把"哪里需要更新"的决定权交给算法,避免不了的重渲染就用时间来换空间(Fiber)

核心答案框架:

  1. 虚拟 DOM + Diff → 减少 DOM 操作
  2. Fiber 架构 → 可中断的更新,不阻塞用户交互
  3. 调度机制 → 优先级控制,高优先级任务插队

二、React Fiber —— 最核心的突破

2.1 为什么需要 Fiber?

旧版(Stack Reconciler)的问题:

  • 递归协调,一旦开始更新就无法中断
  • 大组件树更新时,浏览器掉帧(超过 16ms)
  • 用户点击、输入等交互被阻塞

Fiber 的解决方案:

将协调过程拆分成一个个小任务(Fiber节点),任务之间可以中断、恢复、优先级调整

2.2 Fiber 是什么?

数据结构层面: 每个 React 元素对应一个 Fiber 节点,是一个 JS 对象

// 简化的 Fiber 节点结构
{
  // 类型信息
  tag: HostComponent,  // 节点类型(div、span等)
  type: 'div',
  key: null,
  
  // 树结构关系
  return: parentFiber,   // 父节点
  child: firstChildFiber, // 第一个子节点
  sibling: nextSibling,   // 下一个兄弟节点
  
  // 状态
  memoizedState: null,    // 当前渲染的状态(hooks 链表头)
  memoizedProps: null,    // 当前渲染的 props
  pendingProps: null,     // 等待处理的 props
  
  // 副作用
  flags: Update,          // 要执行的操作(增删改)
  subtreeFlags: null,     // 子树需要执行的操作
  
  // 双缓存
  alternate: oldFiber,    // 指向上一棵树的对应节点
}

2.3 Fiber 的工作流程(双缓存机制)

React 在内存中维护两棵 Fiber 树:

  • Current 树:当前屏幕上显示的内容
  • WorkInProgress 树:正在构建的新树(在内存中)
// 简化版 Fiber 工作循环
function workLoop(deadline) {
  let shouldYield = false
  
  while (nextUnitOfWork && !shouldYield) {
    // 执行当前单元,并返回下一个要执行的单元
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
    
    // 检查剩余时间是否足够
    shouldYield = deadline.timeRemaining() < 1
  }
  
  // 如果所有工作完成,提交到 DOM
  if (!nextUnitOfWork && workInProgressRoot) {
    commitRoot()
  }
  
  // 让出控制权,等待下一帧
  requestIdleCallback(workLoop)
}

// 开始工作
requestIdleCallback(workLoop)

关键点:

  • performUnitOfWork 执行一个 Fiber 节点(处理完后返回下一个)
  • 通过 requestIdleCallback(React 实际用的是 MessageChannel + 自己实现的时间切片)在浏览器空闲时执行
  • 每执行完一个节点就检查是否超时,超时则中断

三、React Diff 算法 —— O(n³) → O(n)

3.1 三个大胆的假设

  1. 只比较同层级节点,不跨层比较
  2. 不同类型元素产生不同树(div → p 直接销毁重建)
  3. 通过 key 标识子节点稳定性

3.2 Diff 流程

// 简化的 reconcileChildFibers 逻辑
function reconcileChildren(returnFiber, currentFirstChild, newChildren) {
  // 情况1:新节点是单节点(原生元素/组件)
  if (isSingleElement(newChildren)) {
    // 尝试复用(key 和 type 都相同)
    if (currentFirstChild && currentFirstChild.key === newChildren.key) {
      if (currentFirstChild.type === newChildren.type) {
        // 复用,更新 props
        return existingFiber
      } else {
        // 类型不同,删除旧节点,创建新节点
        deleteRemainingChildren(currentFirstChild)
        return createNewFiber(newChildren)
      }
    }
  }
  
  // 情况2:新节点是多节点(数组)
  // 使用三指针遍历
  let newIdx = 0
  let oldFiber = currentFirstChild
  let lastPlacedIndex = 0  // 记录上次可复用节点的位置
  
  // 第一轮:同时遍历新旧节点,遇到 key 不同的就跳出
  for (; oldFiber && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.key !== newChildren[newIdx].key) {
      break
    }
    if (oldFiber.type === newChildren[newIdx].type) {
      // 可复用,更新位置标记
      lastPlacedIndex = Math.max(lastPlacedIndex, oldFiber.index)
    }
    oldFiber = oldFiber.sibling
  }
  
  // 第二轮:处理剩余节点(标记删除或移动)
  if (newIdx === newChildren.length) {
    // 新节点遍历完,删除剩余旧节点
    deleteRemainingChildren(oldFiber)
  }
}

3.3 为什么不建议用 index 作为 key?

// 列表:[A, B, C]
// 头部插入 D → [D, A, B, C]

// 用 index 作为 key
// A(0) → D(0)  key 相同但内容不同,无法复用
// B(1) → A(1)  key 相同但内容不同,无法复用
// 结果:全部重建

// 用 id 作为 key
// A(id_a) → A(id_a) 复用
// B(id_b) → B(id_b) 复用
// C(id_c) → C(id_c) 复用
// 只新增 D,其他全部复用

四、Hooks 原理 —— 为什么不能在条件语句中使用?

4.1 Hooks 的本质

Hooks 是一个存储在 Fiber 节点上的链表

// Fiber 节点中的 memoizedState 指向 hooks 链表
{
  memoizedState: {           // 第一个 hook
    memoizedState: 'value',  // hook 存储的值(useState 的 state)
    next: {                  // 第二个 hook
      memoizedState: 'value2',
      next: { ... }          // 第三个 hook
    }
  }
}

4.2 useState 简化实现

// React 内部简化版
let currentlyRenderingFiber = null
let workInProgressHook = null

function renderWithHooks(fiber) {
  currentlyRenderingFiber = fiber
  currentlyRenderingFiber.memoizedState = null
  workInProgressHook = null
  // ... 执行组件函数
}

function useState(initialState) {
  // 获取当前 hook
  const hook = getOrCreateHook()
  
  // 如果是首次渲染,初始化 state
  if (hook.memoizedState === undefined) {
    hook.memoizedState = typeof initialState === 'function' 
      ? initialState() 
      : initialState
  }
  
  // 定义 setter
  const setState = (action) => {
    const newState = typeof action === 'function' 
      ? action(hook.memoizedState) 
      : action
    
    hook.memoizedState = newState
    
    // 触发更新(将当前 fiber 标记为需要更新)
    scheduleUpdateOnFiber(currentlyRenderingFiber)
  }
  
  return [hook.memoizedState, setState]
}

function getOrCreateHook() {
  if (workInProgressHook === null) {
    // 第一个 hook
    workInProgressHook = currentlyRenderingFiber.memoizedState
    return workInProgressHook
  } else {
    // 后续 hook
    workInProgressHook = workInProgressHook.next
    return workInProgressHook
  }
}

4.3 为什么不能在条件语句中调用 Hook?

// ❌ 错误
function Component({ flag }) {
  const [a, setA] = useState(0)
  if (flag) {
    const [b, setB] = useState(1)  // 条件性调用
  }
  const [c, setC] = useState(2)
}

// 第一次渲染 flag=true:hooks 链表 [a, b, c]
// 第二次渲染 flag=false:hooks 链表 [a, c],但 React 以为第三个 hook 是 c
// 结果:状态错乱(c 拿到了 b 的状态)

React 依赖 Hook 的调用顺序来匹配状态,破坏顺序就会出 Bug。


五、调度机制(Scheduler)

5.1 优先级体系

// React 内部的优先级定义
const ImmediatePriority = 1    // 立即执行(用户点击、输入)
const UserBlockingPriority = 2 // 用户交互(hover、滚动)
const NormalPriority = 3       // 普通更新(setState)
const LowPriority = 4          // 低优先级(数据预取)
const IdlePriority = 5         // 空闲执行(日志上报)

5.2 时间切片 + 优先级插队

// 简化版调度逻辑
function ensureRootIsScheduled(root) {
  const existingCallback = root.callbackNode
  
  // 获取当前最高优先级的更新
  const newCallbackPriority = getHighestPriorityUpdate(root)
  
  // 如果没有更高优先级的任务,返回
  if (newCallbackPriority === existingCallbackPriority) return
  
  // 取消正在进行的任务(可中断)
  if (existingCallback) {
    cancelCallback(existingCallback)
  }
  
  // 根据优先级决定调度方式
  let newCallback
  if (newCallbackPriority === ImmediatePriority) {
    // 同步执行,不等待
    newCallback = scheduleSyncCallback(performWorkOnRoot.bind(null, root))
  } else {
    // 异步执行,使用 MessageChannel 实现宏任务
    newCallback = scheduleCallback(
      newCallbackPriority,
      performConcurrentWorkOnRoot.bind(null, root)
    )
  }
  
  root.callbackNode = newCallback
}

六、Vue 响应式原理(对照学习)

6.1 Vue 2 vs Vue 3

特性Vue 2 (Object.defineProperty)Vue 3 (Proxy)
监听数组需要重写 7 个数组方法直接支持
新增属性需要 $set自动响应
删除属性无法监听可监听
性能递归遍历所有属性懒代理,访问时才递归

6.2 Proxy 简化实现

function reactive(target) {
  if (typeof target !== 'object' || target === null) {
    return target
  }
  
  const handler = {
    get(obj, key, receiver) {
      console.log(`读取 ${key}`)
      // 依赖收集(track)
      track(obj, key)
      
      const result = Reflect.get(obj, key, receiver)
      // 深度代理(懒代理,性能更好)
      return typeof result === 'object' ? reactive(result) : result
    },
    
    set(obj, key, value, receiver) {
      const oldValue = obj[key]
      const result = Reflect.set(obj, key, value, receiver)
      
      if (oldValue !== value) {
        console.log(`修改 ${key} = ${value}`)
        // 触发更新(trigger)
        trigger(obj, key)
      }
      
      return result
    },
    
    deleteProperty(obj, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(obj, key)
      const result = Reflect.deleteProperty(obj, key)
      
      if (hadKey && result) {
        trigger(obj, key)
      }
      
      return result
    }
  }
  
  return new Proxy(target, handler)
}

七、面试总结

当被问到 "React 为什么快" 时,回答话术:

"严格来说 React 并不是'快',而是'感知上流畅'。它做了三件事:

  1. Fiber 架构:把更新拆成可中断的小任务,用 requestIdleCallback 在浏览器空闲时执行,避免掉帧。
  2. Diff 算法:基于三个假设将 O(n³) 降为 O(n),通过 key 最大化复用 DOM。
  3. 优先级调度:用户交互(点击、输入)优先级高于普通 setState,高优先级可以打断低优先级更新。

但 React 也有代价——它无法做到细粒度的更新,Vue 的响应式系统在这方面更精确。React 是用'按需计算 + 时间切片'换来了复杂应用的可维护性。"

常见追问及回答:

追问回答要点
"Fiber 和普通递归有什么区别?"递归不可中断,Fiber 用链表 + 循环 + 时间检查实现可中断
"为什么不用 Web Worker?"DOM 操作必须在主线程,Web Worker 无法操作 DOM
"React 18 的并发有什么新东西?"startTransition、useTransition、useDeferredValue 让开发者主动标记非紧急更新