手写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官方文档
相关推荐
xxy-mm19 分钟前
Javascript 中的继承
开发语言·javascript·ecmascript
锋行天下2 小时前
公司内网部署大模型的探索之路
前端·人工智能·后端
1024肥宅3 小时前
手写 EventEmitter:深入理解发布订阅模式
前端·javascript·eventbus
海市公约4 小时前
HTML网页开发从入门到精通:从标签到表单的完整指南
前端·ide·vscode·程序人生·架构·前端框架·html
3秒一个大4 小时前
HTML5 与 JavaScript 中的二进制数据处理:ArrayBuffer 与 TextEncoder/Decoder 实践
javascript
purpleseashell_Lili4 小时前
如何学习 AG-UI 和 CopilotKit
javascript·typescript·react
行云流水6264 小时前
前端树形结构实现勾选,半勾选,取消勾选。
前端·算法
diudiu_335 小时前
web漏洞--认证缺陷
java·前端·网络
阿珊和她的猫5 小时前
<video>` 和 `<audio>` 标签的常用属性解析
前端
LSL666_5 小时前
4 jQuery、JavaScript 作用域、闭包与 DOM 事件绑定
前端·javascript·html