实现 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])
关键实现点
- 存储结构:在 fiber 节点存储 effectHooks 数组
- 执行时机:在 DOM 提交后(commitRoot)执行
- 依赖对比:使用
some()对比新旧依赖项 - 索引匹配:多个 effect 通过数组索引匹配新旧钩子
- 重置机制:每次渲染前清空 effectHooks 数组
完整实现处理了 effect 的初始化、更新和清理逻辑,为函数组件提供了副作用管理能力。