一. 往期文章推荐
强烈推荐阅读第一篇文章手写mini React,理解React渲染原理,有助于理解本文章内容
1.1 React原理系列总结
二. useRef
方法介绍
useRef
方法接收一个可选初始值入参,会赋值给ref
的current
属性,可以通过ref.current
获取属性值或修改属性值,需要注意的是修改ref.current
属性值不会触发更新渲染逻辑
例如下面这段代码调用useRef
方法创建ref
对象,赋值给h1
标签的ref
属性,在更新DOM
阶段将h1
标签DOM
节点赋值给ref.current
属性,在执行useEffect
的create
方法时,控制台会输出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
对象赋值给hook
的memoizedState
属性
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()
}
}