手写React useEffect方法,理解useEffect原理

一. 往期文章推荐

1.1 手写mini React,理解React渲染原理
1.2 手写React useState,理解useState原理

二. useEffect方法介绍

useEffect方法接收两个参数,第一个是执行函数create,第二个是依赖deps,在首次渲染时会执行一次create方法,在下次渲染时会比对deps值是否变更,如果有会再次执行create方法

我们可以在create方法里返回一个函数destroydestroy方法会在deps值发生变化或组件卸载时执行。

例如下面这段代码,在首次渲染时控制台会输出HelloWorld MountApp Mount,在点击click按钮时将visible设置为false,会触发更新渲染,控制台会输出HelloWorld Unmount

javascript 复制代码
function HelloWorld() {
  useEffect(() => {
    console.log('HelloWorld Mount')
    
    return () => {
      console.log('HelloWorld Unmount')
    }
  }, [])
  
  return <h1>hello world</h1>
}

function App() {
  const [visible, useVisible] = useState(true)
  
  useEffect(() => {
    console.log('App Mount')
  }, [])
  
  return (
    <div>
      <button onClick={() => setVisible(!visible)}>click</button>
      {visible && <HelloWorld />}
    </div>
  ) 
}

三. 实现useEffect

3.1 定义Hook对象原型

每次调用useEffect方法时都会创建一个hook对象,多个hook对象通过next指针索引,构建单链表数据结构

javascript 复制代码
function Hook() {
  this.memoizedState = null // 记录hook数据
  this.next = null // 记录下一个hook
  this.queue = [] // 收集更新state方法
}

3.2 修改FiberNode对象原型

新增updateQueue属性,记录useEffect数据,如执行方法create,依赖deps和调用create方法返回的destroy方法

javascript 复制代码
function FiberNode() {
  this.updateQueue = null // 记录useEffect数据
}

3.3 定义函数组件方法调用装饰器

当每次调用函数组件方法(例如App Compoent Function)时会执行renderWithHooks方法,记录新FiberNode节点、旧FiberNode节点的hook链表节点,在调用useEffect方法时会用到

javascript 复制代码
// 记录新FiberNode节点
let currentlyRenderingFiber = null
// 记录旧FiberNode节点的hook链表节点
let currentHook = null
// 记录新FiberNode节点hook链表节点
let workInProgressHook = null

/** 
 * @param {*} current 旧FiberNode节点
 * @param {*} workInProgress 新FiberNode节点
 * @param {*} Component 函数组件方法
 * @param {*} props 函数组件方法入参属性
*/
export function renderWithHooks(current, workInProgress, Component, props) {
  // 记录新FiberNode节点
  currentlyRenderingFiber = workInProgress
  if (current !== null) {
    // 记录旧FiberNode节点的hook链表
    currentHook = current.memoizedState
  }
  workInProgress.updateQueue = null
  // 调用组件方法获取child ReactElement
  const children = Component(props)
  currentlyRenderingFiber = null
  currentHook = null
  workInProgressHook = null
  return children
}

3.4 首次调用useEffect方法

当首次执行函数组件方法,调用useEffect方法时会执行mountEffect方法逻辑,创建hook对象,将useEffect的入参赋值给hookmemoizedState属性和FiberNode节点的updateQueue属性

javascript 复制代码
function mountWorkInProgressHook() {
  const hook = new Hook()
  // 构建hook链表
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook = workInProgressHook.next = hook
  }
  return hook
}

function pushEffect(create, deps, destroy = null) {
  const effect = { create, deps, destroy }
  // 将effect添加到FiberNode节点updateQueue属性中,在更新DOM阶段执行
  if (currentlyRenderingFiber.updateQueue === null)
    currentlyRenderingFiber.updateQueue = []
  const queue = currentlyRenderingFiber.updateQueue
  queue.push(effect)
  return effect
}

function mountEffect(create, deps) {
  // 创建hook对象,构建hook单链表
  const hook = mountWorkInProgressHook()
  // 将当前FiberNode节点的flags赋值为Passive,flags属性表示副作用,例如更新,删除等,在更新DOM阶段会根据flags属性值执行对应的副作用逻辑
  currentlyRenderingFiber.flags |= Passive
  hook.memoizedState = pushEffect(create, deps)
}

3.5 调用useEffect create方法

递归遍历FiberNode节点,判断flags属性值是否有Passive,如果有则遍历该节点updateQueue属性值,调用useEffectcreate方法

javascript 复制代码
// 遍历调用useEffect的create方法,获取destroy方法
function commitHookPassiveMountEffects(finishWork) {
  const queue = finishWork.updateQueue
  queue.forEach((effect) => {
    effect.destroy = effect.create()
  })
}

function recursivelyTraversePassiveMountEffects(finishWork) {
  if (finishWork.subtreeFlags & Passive) {
    let child = finishWork.child
    while (child !== null) {
      commitPassiveMountOnFiber(child)
      child = child.sibling
    }
  }
}

function commitPassiveMountOnFiber(finishWork) {
  switch (finishWork.tag) {
    case FunctionComponent: {
      recursivelyTraversePassiveMountEffects(finishWork)
      if (finishWork.flags & Passive) {
        // 调用useEffeact的create方法
        commitHookPassiveMountEffects(finishWork)
      }
      break
    }
    default: {
      recursivelyTraversePassiveMountEffects(finishWork)
      break
    }
  }
}

3.6 更新调用useEffect方法

当触发更新渲染重新调用useEffect方法时,会比对deps是否变更,如果没有变更则不需要将FiberNode节点flags属性赋值为Passive,即在更新DOM阶段不会执行useEffect create方法,如果deps变更则需要将flags属性赋值为Passive

javascript 复制代码
// 比对deps属性值是否变更
function areHookInputsEqual(nextDeps, prevDeps) {
  for (let i = 0; i < nextDeps.length; i++) {
    if (!Object.is(nextDeps[i], prevDeps[i])) {
      return false
    }
  }
  return true
}

function updateEffect(create, deps) {
  const hook = mountWorkInProgressHook()
  // 获取旧useEffect入参数据
  const effect = currentHook.memoizedState
  if (deps !== null && areHookInputsEqual(deps, effect.deps)) {
    hook.memoizedState = pushEffect(create, deps, effect.destroy)
    return
  }
  currentlyRenderingFiber.flags |= Passive
  hook.memoizedState = pushEffect(create, deps)
}

3.7 更新调用useEffect destory方法

递归遍历FiberNode节点,判断flags属性值是否有Passive,如果有则遍历该节点updateQueue属性中的effect,调用useEffectdestroy方法

如果FiberNode节点的deletions属性不为空,说明有child FiberNode节点被删除,则递归遍历FiberNode节点deletions属性中的child FiberNode节点,调用对应useEffectdestroy方法

javascript 复制代码
// 遍历调用useEffect的destroy方法,将effect的destroy属性赋值为null
function commitHookPassiveUnmountEffects(finishWork) {
  const queue = finishWork.updateQueue
  if (queue !== null) {
    queue.forEach((effect) => {
      if (effect.destroy) {
        const destroy = effect.destroy
        effect.destroy = null
        destroy()
      }
    })
  }
}

function recursivelyTraversePassiveUnmountEffects(finishWork) {
  if (finishWork.deletions !== null) {
    // 采用深度优先遍历算法,优先执行分支叶子节点useEffect的destroy方法
    for (let i = 0; i < finishWork.deletions.length; i++) {
      let fiber = finishWork.deletions[i]
      while (true) {
        let nextChild = fiber.child
        while (nextChild !== null) {
          fiber = nextChild
          nextChild = nextChild.child
        }
        commitHookPassiveUnmountEffects(fiber)
        if (fiber.sibling !== null) {
          nextChild = fiber.sibling
          fiber.sibling = null
        } else {
          if (fiber === finishWork.deletions[i]) break
          fiber = fiber.return
          fiber.child = null
        }
      }
    }
  }
  if (finishWork.subtreeFlags & (Passive | ChildDeletion)) {
    let child = finishWork.child
    while (child !== null) {
      commitPassiveUnmountOnFiber(child)
      child = child.sibling
    }
  }
}

function commitPassiveUnmountOnFiber(finishWork) {
  switch (finishWork.tag) {
    case FunctionComponent: {
      recursivelyTraversePassiveUnmountEffects(finishWork)
      if (finishWork.flags & Passive) {
        commitHookPassiveUnmountEffects(finishWork)
      }
      break
    }
    default: {
      recursivelyTraversePassiveUnmountEffects(finishWork)
      break
    }
  }
}

3.8 定义useEffect方法

如果新节点不存在旧FiberNode节点,说明是首次调用函数组件方法,则调用mountEffect方法,否则调用updateEffect方法

javascript 复制代码
function useEffect(create, deps = null) {
  const current = currentlyRenderingFiber.alternate
  if (current === null) {
    mountEffect(create, deps)
  } else {
    updateEffect(create, deps)
  }
}

四. 参考文档

4.1 React useEffect官方文档
相关推荐
weixin_382395237 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__7 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.8 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~9 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华10 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子12 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅12 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni12 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学14 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端