实现 Mini-React 的 useEffect 钩子

32 阅读2分钟

实现 Mini-React 的 useEffect 钩子:从单次执行到依赖追踪

内容概括
本文实现了 Mini-React 的 useEffect 钩子,首先处理无依赖项的基本回调执行,通过在 commit 阶段添加 effectHook 调用。随后扩展支持依赖项对比,在更新时检查 deps 变化决定是否执行。最后实现多个 useEffect 共存,使用数组存储并索引对比新旧依赖。核心是在 fiber 节点存储 effectHooks 数组,commitRoot 后遍历执行,初次渲染直接执行,更新时通过 some() 对比依赖变化。


实现 Mini-React 的 useEffect 钩子

本文将逐步实现一个简化版 React 的 useEffect 钩子,从基础执行到依赖项追踪,最终支持多个 effect 共存。

1. 基础 useEffect 实现

// react.js
function useEffect(callback, deps) {
  const effectHook = { callback, deps }
  wipFiber.effectHook = effectHook
}

function commitRoot() {
  commitWork(wipRoot.child)
  commitEffectHook() // DOM 提交后执行 effect
}

function commitEffectHook() {
  const run = (fiber) => {
    if (!fiber) return
    fiber.effectHook?.callback() // 直接执行回调
    run(fiber.child)
    run(fiber.sibling)
  }
  run(wipFiber)
}

在组件中调用:

// App.js
React.useEffect(() => {
  console.log('Mounted!')
}, [])

2. 添加依赖项追踪

function commitEffectHook() {
  const run = (fiber) => {
    if (!fiber) return
    
    if (!fiber.alternate) {
      // 初次渲染直接执行
      fiber.effectHook?.callback()
    } else {
      // 对比依赖变化
      const oldHook = fiber.alternate.effectHook
      const needUpdate = oldHook.deps.some(
        (dep, i) => dep !== fiber.effectHook.deps[i]
      )
      needUpdate && fiber.effectHook.callback()
    }
    
    run(fiber.child)
    run(fiber.sibling)
  }
  run(wipFiber)
}

组件使用:

const [count, setCount] = React.useState(0)

React.useEffect(() => {
  console.log('Count updated:', count)
}, [count]) // 仅当 count 变化时执行

3. 支持多个 useEffect

// react.js
let effectHooks = []

function useEffect(callback, deps) {
  effectHooks.push({ callback, deps })
  wipFiber.effectHooks = effectHooks
}

function commitEffectHooks() {
  const run = (fiber) => {
    // ...
    const hooks = fiber.effectHooks || []
    
    if (!fiber.alternate) {
      hooks.forEach(hook => hook.callback())
    } else {
      const oldHooks = fiber.alternate.effectHooks || []
      hooks.forEach((newHook, i) => {
        if (newHook.deps.length > 0) {
          const needUpdate = oldHooks[i]?.deps.some(
            (d, idx) => d !== newHook.deps[idx]
          )
          needUpdate && newHook.callback()
        }
      })
    }
    // ...
  }
}

组件中使用多个 effect:

React.useEffect(() => console.log('Mounted'), [])

React.useEffect(() => {
  console.log('Count updated', count)
}, [count])

关键实现点

  1. 存储结构:在 fiber 节点存储 effectHooks 数组
  2. 执行时机:在 DOM 提交后(commitRoot)执行
  3. 依赖对比:使用 some() 对比新旧依赖项
  4. 索引匹配:多个 effect 通过数组索引匹配新旧钩子
  5. 重置机制:每次渲染前清空 effectHooks 数组

完整实现处理了 effect 的初始化、更新和清理逻辑,为函数组件提供了副作用管理能力。