手写React useRef,理解useRef原理

114 阅读3分钟

一. 往期文章推荐

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

1.1 React原理系列总结

二. useRef方法介绍

useRef方法接收一个可选初始值入参,会赋值给refcurrent属性,可以通过ref.current获取属性值或修改属性值,需要注意的是修改ref.current属性值不会触发更新渲染逻辑

例如下面这段代码调用useRef方法创建ref对象,赋值给h1标签的ref属性,在更新DOM阶段将h1标签DOM节点赋值给ref.current属性,在执行useEffectcreate方法时,控制台会输出h1标签DOM节点

function App() {
  const elRef = useRef(null)
  
  useEffect(() => {
    console.log(elRef.current)
  }, [])
  
  return <h1 ref={elRef}>hello world</h1>
}

三. 实现useRef

3.1 定义Hook对象原型

每次调用useRef方法时都会创建一个Hook对象,多个Hook对象通过next指针索引,构建单链表数据结构

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

3.2 修改FiberNode对象原型

新增ref属性记录useRef数据

function FiberNode() {
  this.ref = null
}

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

当每次调用函数组件方法(例如App Compoent Function)时会执行renderWithHooks方法,记录新FiberNode节点,在调用useRef方法时会用到

// 记录新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
  workInProgress.updateQueue = null
  // 调用组件方法获取child ReactElement
  const children = Component(props)
  currentlyRenderingFiber = null
  currentHook = null
  workInProgressHook = null
  return children
}

3.4 首次调用useRef

当首次执行函数组件方法,调用useRef方法时会执行mountRef方法逻辑,创建hook对象,将初始值赋值给ref.current属性,接着将ref对象赋值给hookmemoizedState属性

function mountWorkInProgressHook() {
  const hook = new Hook()
  // 构建hook链表
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook = workInProgressHook.next = hook
  }
  return hook
}

function mountRef(initialValue) {
  const hook = mountWorkInProgressHook()
  const ref = { current: initialValue }
  return (hook.memoizedState = ref)
}

3.5 修改FiberNode节点ref属性

在构建FiberNode Tree的时候,会获取ReactElement对象props属性中的ref对象,赋值给FiberNode节点的ref属性

function coerceRef(fiber, element) {
  const ref = element.props.ref
  fiber.ref = ref || null
}

3.6 修改FiberNode节点flags属性

如果新节点不存在ref对象而旧节点存在,需要将FiberNode节点的flags属性赋值为Ref,即更新DOM节点阶段需要处理ref effect

如果新节点ref对象且与旧节点ref对象不相同,需要将FiberNode节点的flags属性赋值为Ref,即更新DOM节点阶段需要处理ref effect

function markRef(current, workInProgress) {
  if (workInProgress.ref === null) {
    // 说明旧节点存在ref对象而新节点没有,例如h1标签一开始有ref属性,后面没有了
    if (current !== null && current.ref !== null) {
      workInProgress.flags |= Ref
    }
  } else {
    // 说明新节点ref对象有变更,例如h1标签的ref属性值发生变更
    if (current === null || current.ref !== workInProgress.ref) {
      workInProgress.flags |= Ref
    }
  }
}

3.7 修改FiberNode节点ref属性

递归遍历FiberNode节点,判断flags属性值是否有Ref,如果有则将节点stateNode属性值即DOM节点赋值给ref.current,注意需要先将ref.current属性值赋值为null

function commitAttachRef(finishWork) {
  const { ref, stateNode } = finishWork
  // 先将ref.current赋值为null然后重新赋值
  if (alternate !== null && alternate.ref !== null) alternate.ref.current = null
  if (ref !== null) ref.current = stateNode
}

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

export function commitLayoutEffectOnFiber(finishWork) {
  switch (finishWork.tag) {
    case HostComponent: {
      recursivelyTraverseLayoutEffects(finishWork)
      if (finishWork.flags & Ref) commitAttachRef(finishWork)
      break
    }
    default: {
      recursivelyTraverseLayoutEffects(finishWork)
      break
    }
  }
}

3.8 更新调用useRef方法

获取旧FiberNode节点的hook链表节点,创建新Hook对象,复制旧Hook对象属性值,构建新hook链表,返回ref对象

function updateWorkInProgressHook() {
  if (currentHook === null) {
    // 记录旧FiberNode节点hook链表节点
    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
}

function updateRef() {
  const hook = updateWorkInProgressHook()
  return hook.memoizedState
}

3.9 定义useRef方法

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

function useRef(initialValue = null) {
  const current = currentlyRenderingFiber.alternate
  if (current === null) {
    return mountRef(initialValue)
  } else {
    return updateRef()
  }
}

四. 参考文档

4.1 React useRef官方文档