本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React
内部机制。
由于React
源码通过Mono-repo 管理仓库,我们也是用pnpm
提供的workspaces
来管理我们的代码仓库,打包我们使用rollup
进行打包。
系列文章:
- React实现系列一 - jsx
- 剖析React系列二-reconciler
- 剖析React系列三-打标记
- 剖析React系列四-commit
- 剖析React系列五-update流程
- 剖析React系列六-dispatch update流程
- 剖析React系列七-事件系统
- 剖析React系列八-同级节点diff
- 剖析React系列九-Fragment的部分实现逻辑
- 剖析React系列十- 调度<合并更新、优先级>
- 剖析React系列十一- useEffect的实现原理
- 剖析React系列十二-调度器的实现
- 剖析React系列十三-react调度
- useTransition的实现
本章我们来讲解useRef
的使用和实现原理。
useRef的使用
useRef
的使用非常简单,我们可以通过useRef
来获取一个可变的ref
对象,ref
对象的.current
属性在重复渲染的过程中,会保持引用不变,我们可以通过ref
对象来获取dom
节点,或者在重复渲染中不变的值。
useRef
接受一个初始值, 返回一个对象,包含.current
属性。
let ref = useRef(null)
通常我们用它去获取渲染的dom
节点, 有一下2种方式:
// 第一种:直接赋值给ReactElement的ref属性
let ref = useRef(null)
function App() {
return <div ref={ref}>hello world</div>
}
// 第二种:通过函数的形式, 将dom传递给函数的参数
let ref = useRef(null)
function App() {
return <div ref={(dom) => {ref.current = dom}}>hello world</div>
}
接下来我们来看看useRef
的实现原理。
获取ref值
当我们传递给reactElement
的ref
属性的时候,首先我们将其ref
的属性值赋值到对应的fiber
节点上。
所以我们要修改fiber.ts
中的createFiberFromElement
以及createWorkInProgress
方法。
export function createFiberFromElement(element: ReactElementType): FiberNode {
+ const { type, key, props, ref } = element;
let fiberTag: WorkTag = FunctionComponent;
...
+ fiber.ref = ref;
return fiber;
}
export const createWorkInProgress = (
current: FiberNode,
pendingProps: Props
): FiberNode => {
let wip = current.alternate;
...
+ wip.ref = current.ref;
return wip;
};
这样我们在进行调和reconciler
的时候,就可以获取到ref
的值了。
reconciler
阶段
之前的章节我们晓得调和阶段分为2个阶段:
beginWork
阶段completeWork
阶段
对于ref
的处理,这2个阶段主要是针对有ref
属性的fiber
进行打标记处理。
正常情况下,我们只需要在beginWork
中打好标记,这里的completeWork
中是兜底操作。
新增一个flag
类型
我们在fiberFlags
中新增一个ref
类型,用来表示ref
的打标记。同时新增一个LayoutMask
,用来表示layout
阶段要执行的标志。
export const Ref = 0b0010000; // ref
export const LayoutMask = Ref; // layout阶段要执行的标志
新增了一个Ref
的标志后,我们接下来就需要根据条件判断是否要打上这个标记。
beginWork
阶段
在beginWork
阶段,我们需要对2种情况进行处理:
- 刚刚初始化的时候,
wip.alternate
为null
,这个时候我们需要判断fiber
节点是否有ref
属性,如果有,就打上Ref
标记。 wip.alternate
不为null
,这个时候我们需要判断fiber
节点的ref
属性是否发生变化,如果发生变化,就打上Ref
标记。
在调和的过程中,如果遇到wip.tag
为updateHostComponent
的时候,标识这是一个dom
类型的fiber,我们就可以判断是否有ref
属性了。
function updateHostComponent(wip: FiberNode) {
// ....
markRef(wip.alternate, wip);
// ....
return wip.child;
}
/**
* 标记Ref
*/
function markRef(current: FiberNode | null, workInProgress: FiberNode) {
const ref = workInProgress.ref;
// mount时 有ref 或者 update时 ref变化
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
workInProgress.flags |= Ref;
}
}
completeWork
阶段
在completeWork
阶段,主要是进行兜底操作。
由于本身已经区分了初始化和更新的情况,所以我们只需要在不同的情况下判断即可, 所以针对HostComponent
类型进行如下判断:
// 更新阶段
// 标记ref
if (current.ref !== wip.ref) {
markRef(wip);
}
// 初始化阶段
// 标记Ref
if (wip.ref !== null) {
markRef(wip);
}
function markRef(fiber: FiberNode) {
fiber.flags |= Ref;
}
commit
阶段
在commit
阶段,我们需要对Ref
标记进行处理,在之前的commit阶段文章,我们了解到commit
分为三个子阶段:
beforeMutation
阶段mutation
阶段layout
阶段 针对ref
的处理,主要是在mutation
子阶段进行原有的ref
值的解绑,layout
阶段需要绑定新的值。
解绑和绑定
对于mutation
阶段的解绑操作, 获取到ref
的值,然后判断是否是函数,如果是函数的话就传递null
,否者,就将current
设置为null
。
// 解绑之前的Ref
if ((flags & Ref) !== NoFlags && tag === HostComponent) {
safelyDetachRef(finishedWork);
}
/**
* 解绑当前的ref
*/
function safelyDetachRef(current: FiberNode) {
const ref = current.ref;
if (ref !== null) {
if (typeof ref === "function") {
ref(null);
} else {
ref.current = null;
}
}
}
在layout
阶段进行重新的绑定。
if ((flags & Ref) !== NoFlags && tag === HostComponent) {
// 绑定新的ref
safelyAttachRef(finishedWork);
}
/**
* 绑定新的ref
*/
function safelyAttachRef(fiber: FiberNode) {
const ref = fiber.ref;
if (ref !== null) {
const instance = fiber.stateNode;
if (typeof ref === "function") {
ref(instance);
} else {
ref.current = instance;
}
}
}
卸载
当被绑定的fiber
节点被卸载的时候,我们需要对ref
进行解绑操作。 我们知道在卸载的时候会执行commitDeletion
, 所以针对hostComponent
类型的fiber
节点,我们需要进行解绑操作。
function commitDeletion(childToDelete: FiberNode, root: FiberRootNode) {
const rootChildrenToDelete: FiberNode[] = [];
// 递归子树
commitNestedComponent(childToDelete, (unmountFiber) => {
switch (unmountFiber.tag) {
case HostComponent:
// xxxxxx
safelyDetachRef(unmountFiber);
return;
}
// xxxxx
})
}
ref重复渲染值不变
我们知道在重复渲染的时候ref.current
的值是保持不变的,那么它是如何实现的呢? 我们来看看useRef
的2个阶段的实现代码。
/**
* useRef使用 ref = useRef(null)
* @param initialValue
*/
function mountRef<T>(initialValue: T): { current: T } {
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };
hook.memoizedState = ref;
return ref;
}
function updateRef<T>(initialValue: T): { current: T } {
const hook = updateWorkInProgressHook();
return hook.memoizedState;
}
在初始化的时候调用mountRef
函数。我们创建一个{current: initialValue}
的对象,然后将这个对象赋值给hook.memoizedState
。
对于不同的hook,都是通过对于的hook.memoizedState
来保存数据的。
在更新的时候,我们调用updateRef
函数,这个时候我们直接返回hook.memoizedState
,也就是说,我们返回的是同一个对象,所以ref.current
的值是不会变化的。
总结
至此,函数组件针对ref
的2种处理就都完成了。
通过上面的讲解,我们应该知道了,当我们通过函数接受dom
的时候,当触发更新的时候,会触发2次函数的执行,第一次是解绑操作,第二次是绑定操作。
例如点击count
的时候,会输出2次
function App() {
const ref = useRef(null);
const [count, setCount] = useState(1);
return (
<div className="App">
<div
ref={(dom) => {
console.log("dom: --", dom);
}}
>
hcc
</div>
<div
onClick={() => {
setCount(count + 1);
}}
>
{count}
</div>
</div>
);
}