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

一. 往期文章推荐

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

二. useEffect方法介绍

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

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

例如下面这段代码,在首次渲染时控制台会输出HelloWorld Mount、App 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的入参赋值给hook的memoizedState属性和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属性值,调用useEffect的create方法

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,调用useEffect的destroy方法

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

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官方文档
相关推荐
lpfasd1237 小时前
node-sass 停止维护!老项目迁移 Dart Sass 完整教程
前端·rust·sass
Data analyse4569 小时前
rnweb 回放数据量太大怎么办?分片存储与断点续传的实现思路
前端·网络·算法·数据分析
xiaogai_gai9 小时前
金蝶收料通知单查询策略实战教程:从源系统拉取到中间层落地的完整路径
java·前端·数据库
环境栈笔记10 小时前
指纹浏览器 WebRTC 怎么设置?从 ICE 候选、代理出口到功能验证
前端·人工智能·后端·自动化
赵大仁11 小时前
官网性能优化实战:LCP 3.2s → 2.1s、CLS 0.107 → 0,以及被我推翻的 6 个判断
前端·性能优化·cls·lcp·core web vitals
xiancai_xianyu11 小时前
Text2SQL落地生产库前,SQL要过哪三道闸
前端·数据库·sql·sql注入·text2sql·ai agent开发·企业级ai框架
小羊没烦恼!11 小时前
T-SQL查询进阶--理解SQL SERVER中的分区表
服务器·开发语言·前端·javascript·html
史瓦辛格11 小时前
给 DeepSeek Harness 写费用插件:把「这次会话花了多少钱」算准
前端·deepseek
星光不问赶路人11 小时前
Vue 3 + TypeScript 中 enum 的使用实践
前端·vue.js
鸥梨菌Honevid11 小时前
UVM 中 REQ 和 RSP 类型详解:从通信模型到 item_done 与 put_response
java·前端·servlet