手写React useEffect,理解useEffect原理

140 阅读5分钟

一. 往期文章推荐

强烈推荐阅读第一篇文章手写mini React,理解React渲染原理,有助于理解本文章内容

1.1 React原理系列总结

二. useEffect方法介绍

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

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

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

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对象原型

每次调用React Hook方法都会生成一个Hook对象,多个Hook对象之间通过next指针进行索引,构成单链表数据结构

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

3.2 修改FiberNode对象原型

新增updateQueue属性用于收集useEffectuseLayoutEffectuseInsertionEffect数据,例如入参create方法,依赖deps和调用create返回的destroy方法

function FiberNode() {
  this.updateQueue = null // 记录useEffect数据
}

3.3 定义HookFlags枚举值

通过HookFlags区分effect类型,HookInsertion代表useInsertionEffectHookLayout代表useLayoutEffectHookPassive代表useEffect

export const HookHasEffect = 1 // effect通用类型
export const HookInsertion = 2 // 对应useInsertionEffect
export const HookLayout = 4 // 对应useLayoutEffect
export const HookPassive = 8 // 对应useEffect

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

在构建虚拟DOM树阶段,每次调用函数组件方法(例如App Compoent Function)时会执行renderWithHooks方法,记录新FiberNode节点,在调用React Hook方法时会用到

// 记录新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
  // 将FiberNode节点的updateQueue属性赋值为null,重新收集useEffect、useLayoutEffect、useInsertionEffect数据
  workInProgress.updateQueue = null
  // 调用组件方法获取child ReactElement
  const children = Component(props)
  currentlyRenderingFiber = null
  currentHook = null
  workInProgressHook = null
  return children
}

3.5 首次调用useEffect方法

当首次执行组件方法调用useEffect方法时,执行mountEffect方法逻辑

  • FiberNode节点的flags属性赋值为Passive
  • 创建Hook对象,构建Hook链表
  • 创建effect对象,tag属性即effect类型,create即入参执行函数,deps即入参依赖,destroy属性时调用create返回的方法。将effect对象赋值给FiberNode节点的updateQueue属性和Hook对象的memoizedState属性
function mountWorkInProgressHook() {
  const hook = new Hook()
  // 构建Hook链表
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook = workInProgressHook.next = hook
  }
  return hook
}

/**
 * @param {*} tag HookFlags类型
 * @param {*} create 入参执行函数
 * @param {*} deps 入参依赖
 * @param {*} destroy 入参执行函数返回值
 */
function pushEffect(tag, create, deps, destroy = null) {
  const effect = { tag, create, deps, destroy }
  // 将effect添加到FiberNode节点updateQueue属性中,在更新DOM阶段执行
  if (currentlyRenderingFiber.updateQueue === null)
    currentlyRenderingFiber.updateQueue = []
  const queue = currentlyRenderingFiber.updateQueue
  queue.push(effect)
  return effect
}

/**
 * @param {*} fiberFlags FiberNode节点副作用
 * @param {*} hookFlags Effect类型
 * @param {*} create 入参执行函数
 * @param {*} deps 入参依赖
 */
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
  // 创建Hook对象,构建Hook单链表
  const hook = mountWorkInProgressHook()
  currentlyRenderingFiber.flags |= fiberFlags
  hook.memoizedState = pushEffect(hookFlags | HookHasEffect, create, deps)
}

function mountEffect(create, deps) {
  mountEffectImpl(PassiveEffect, HookPassive, create, deps)
}

3.6 调用useEffect create方法

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

// 遍历调用useEffect的create方法,获取destroy方法
function commitHookPassiveMountEffects(finishWork, hookFlags) {
  const queue = finishWork.updateQueue
  queue.forEach((effect) => {
    if ((effect.tag & hookFlags) === hookFlags) {
      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) {
        // 调用useEffect的create方法
        commitHookPassiveMountEffects(finishWork, HookPassive | HookHasEffect)
      }
      break
    }
    default: {
      recursivelyTraversePassiveMountEffects(finishWork)
      break
    }
  }
}

3.7 更新调用useEffect方法

  • 创建Hook对象,复制旧Hook对象属性值,构建Hook链表
  • 比对新旧effect对象的dpes属性值是否相同,相同则不需要将FiberNode节点的flags属性值赋值为Passive,即在更新DOM阶段不会执行effect create方法,如果不相同则需要将flags属性赋值为Passive
  • 创建新的effect对象赋值给FiberNode节点的updateQueue属性和Hook对象的memoizedState属性
function updateWorkInProgressHook() {
  if (currentHook === null) {
    currentHook = currentlyRenderingFiber.alternate.memoizedState
  } else {
    currentHook = currentHook.next
  }
  const hook = new Hook()
  hook.memoizedState = currentHook.memoizedState
  hook.queue = currentHook.queue
  // 构建hook链表
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook = workInProgressHook.next = hook
  }
  return hook
}

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

/**
 * @param {*} fiberFlags FiberNode节点副作用
 * @param {*} hookFlags Effect类型
 * @param {*} create 入参执行函数
 * @param {*} deps 入参依赖
 */
function updateEffectImpl(fiberFlags, hookFlags, create, deps) {
  // 创建Hook对象,构建Hook单链表
  const hook = updateWorkInProgressHook()
  // 获取旧Effect对象
  const effect = hook.memoizedState
  // 判断新旧Effect deps是否相同
  if (deps !== null && areHookInputsEqual(deps, effect.deps)) {
    hook.memoizedState = pushEffect(hookFlags, create, deps, effect.destroy)
    return
  }
  currentlyRenderingFiber.flags |= fiberFlags
  hook.memoizedState = pushEffect(hookFlags | HookHasEffect, create, deps, effect.destroy)
}

function updateEffect(create, deps) {
  updateEffectImpl(PassiveEffect, HookPassive, create, deps)
}

3.8 更新调用useEffect destory方法

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

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

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

function recursivelyTraversePassiveUnmountEffects(finishWork) {
  if (finishWork.deletions !== null) {
    // 采用深度优先遍历算法
    for (let i = 0; i < finishWork.deletions.length; i++) {
      let fiber = finishWork.deletions[i]
      while (true) {
        let nextChild = fiber.child
        commitHookEffectListUnmount(fiber, HookPassive)
        while (nextChild !== null) {
          fiber = nextChild
          commitHookEffectListUnmount(fiber, HookPassive)
          nextChild = nextChild.child
        }
        if (fiber.sibling !== null) {
          nextChild = fiber.sibling
          fiber.sibling = null
          fiber = nextChild
        } 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, HookPassive | HookHasEffect)
      }
      break
    }
    default: {
      recursivelyTraversePassiveUnmountEffects(finishWork)
      break
    }
  }
}

3.9 定义useEffect方法

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

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

四. 参考文档

4.1 React useEffect官方文档