手写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官方文档
相关推荐
x_chengqq2 小时前
前端批量下载文件
前端
捕鲸叉4 小时前
QT自定义工具条渐变背景颜色一例
开发语言·前端·c++·qt
傻小胖5 小时前
路由组件与一般组件的区别
前端·vue.js·react.js
Elena_Lucky_baby5 小时前
在Vue3项目中使用svg-sprite-loader
开发语言·前端·javascript
重生之搬砖忍者6 小时前
uniapp使用canvas生成订单小票图片
前端·javascript·canva可画
万水千山走遍TML6 小时前
console.log封装
前端·javascript·typescript·node·log·console·打印封装
赵大仁6 小时前
uni-app 多平台分享实现指南
javascript·微信小程序·uni-app
阿雄不会写代码6 小时前
使用java springboot 使用 Redis 作为消息队列
前端·bootstrap·html
m0_748236586 小时前
【Nginx 】Nginx 部署前端 vue 项目
前端·vue.js·nginx
@C宝7 小时前
【前端面试题】前端中的两个外边距bug以及什么是BFC
前端·bug