手写React useLayoutEffect,理解useLayoutEffect原理

40 阅读5分钟

一. 往期文章推荐

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

1.1 React原理系列总结

二. useLayoutEffect方法介绍

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

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

useEffect差异点是执行时机不同:useEffect是页面完成渲染之后执行,useLayoutEffect是页面完成渲染之前执行。

三. 实现useLayoutEffect

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
}

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

在构建虚拟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.4 定义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.5 首次调用useLayoutEffect方法

当首次执行组件方法调用useLayoutEffect方法时,执行mountLayoutEffect方法逻辑

  • FiberNode节点的flags属性赋值为Update
  • 创建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 }
  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) {
  const hook = mountWorkInProgressHook()
  currentlyRenderingFiber.flags |= fiberFlags
  hook.memoizedState = pushEffect(hookFlags | HookHasEffect, create, deps)
}

function mountLayoutEffect(create, deps) {
  mountEffectImpl(UpdateEffect, HookLayout, create, deps)
}

3.6 调用useLayoutEffect create方法

在更新DOM阶段,递归遍历FiberNode节点,判断flags属性值是否有Update,如果有则遍历该节点updateQueue属性值,调用effect对象的create方法

// 调用effect create方法,获取destroy方法
function commitHookEffectListMount(finishWork, hookFlags) {
  const queue = finishWork.updateQueue
  queue.forEach((effect) => {
    if ((effect.tag & hookFlags) === hookFlags) {
      effect.destroy = effect.create()
    }
  })
}

function recursivelyTraverseLayoutEffects(finishWork) {
  if (finishWork.subtreeFlags & (Ref | Update)) {
    let child = finishWork.child
    while (child !== null) {
      commitLayoutEffectOnFiber(child)
      child = child.sibling
    }
  }
}

export function commitLayoutEffectOnFiber(finishWork) {
  switch (finishWork.tag) {
    case FunctionComponent: {
      recursivelyTraverseLayoutEffects(finishWork)
      if (finishWork.flags & Update) {
        // 调用useLayoutEffect的create方法
        commitHookEffectListMount(finishWork, HookLayout | HookHasEffect)
      }
      break
    }
    default: {
      recursivelyTraverseLayoutEffects(finishWork)
      break
    }
  }
}

3.7 更新调用useLayoutEffect方法

  • 创建Hook对象,复制旧Hook对象属性值,构建Hook链表
  • 比对新旧effect对象的dpes属性值是否相同,相同则不需要将FiberNode节点的flags属性值赋值为Update,即在更新DOM阶段不会执行effect create方法,如果不相同则需要将flags属性赋值为Update
  • 创建新的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
}

// 通过Object.is方法比对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 updateLayoutEffect(create, deps) {
  updateEffectImpl(UpdateEffect, HookLayout, create, deps)
}

3.8 更新调用useLayoutEffect destory方法

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

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

// 调用effect destroy方法
function commitHookEffectListUnmount(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 recursivelyTraverseDeletionEffects(finishWork) {
  let child = finishWork.child
  while (child !== null) {
    commitDeletionEffectsOnFiber(child)
    child = child.sibling
  }
}

function commitDeletionEffectsOnFiber(finishWork) {
  switch (finishWork.tag) {
    case FunctionComponent:
      commitHookEffectListUnmount(finishWork, HookLayout)
      recursivelyTraverseDeletionEffects(finishWork)
      break
    default:
      recursivelyTraverseDeletionEffects(finishWork)
      break
  }
}

function recursivelyTraverseMutationEffects(finishWork) {
  if (finishWork.deletions !== null) {
    // 递归遍历要删除的child FiberNode节点,调用effect destroy方法
    finishWork.deletions.forEach((fiber) => {
      commitDeletionEffectsOnFiber(finishWork, fiber)
    })
  }
  // 为true说明子树FiberNode节点有副作用需要处理,递归遍历child FiberNode
  if (finishWork.subtreeFlags & (Placement | Update | ChildDeletion)) {
    let child = finishWork.child
    while (child !== null) {
      commitMutationEffectsOnFiber(child)
      child = child.sibling
    }
  }
}

export function commitMutationEffectsOnFiber(finishWork) {
  switch (finishWork.tag) {
    case FunctionComponent: {
      recursivelyTraverseMutationEffects(finishWork)
      if (finishWork.flags & Update) {
        // 调用useLayoutEffect destroy方法
        commitHookEffectListUnmount(finishWork, HookLayout | HookHasEffect)
      }
      break
    }
    default:
      recursivelyTraverseMutationEffects(finishWork)
      break
  }
}

3.9 定义useLayoutEffect方法

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

function useLayoutEffect(create, deps = null) {
  const current = currentlyRenderingFiber.alternate
  if (current === null) {
    mountLayoutEffect(create, deps)
  } else {
    updateLayoutEffect(create, deps)
  }
}

四. 参考文档

4.1 React useLayoutEffect官方文档