ref是reference(引用)的缩写。在React中,我们习惯用ref保存DOM。
事实上,任何需要被"引用"的数据都可以保存在ref中,useRef的出现将这种思想进一步发扬光大。
在Hooks数据结构一节我们讲到:
对于useRef(1),memoizedState保存{current: 1}
本节我们会介绍useRef的实现,以及ref的工作流程。
由于string类型的ref已不推荐使用,所以本节针对function | {current: any}类型的ref。
useRef
与其他Hook
一样,对于mount
与update
,useRef
对应两个不同dispatcher
。
function mountRef<T>(initialValue: T): {|current: T|} {
// 获取当前useRef hook
const hook = mountWorkInProgressHook();
// 创建ref
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
function updateRef<T>(initialValue: T): {|current: T|} {
// 获取当前useRef hook
const hook = updateWorkInProgressHook();
// 返回保存的数据
return hook.memoizedState;
}
你可以在这里看到这段代码
可见,useRef
仅仅是返回一个包含current
属性的对象。
为了验证这个观点,我们再看下React.createRef
方法的实现:
export function createRef(): RefObject {
const refObject = {
current: null,
};
return refObject;
}
你可以从这里看到这段代码
了解了ref
的数据结构后,我们再来看看ref
的工作流程。
ref的工作流程
在React
中,HostComponent
、ClassComponent
、ForwardRef
可以赋值ref
属性。
// HostComponent
<div ref={domRef}></div>
// ClassComponent / ForwardRef
<App ref={cpnRef} />
其中,ForwardRef
只是将ref
作为第二个参数传递下去,不会进入ref
的工作流程。
所以接下来讨论ref
的工作流程时会排除ForwardRef
。
// 对于ForwardRef,secondArg为传递下去的ref
let children = Component(props, secondArg);
你可以在这里看到这段代码
我们知道HostComponent
在commit阶段
的mutation阶段
执行DOM
操作。
所以,对应ref
的更新也是发生在mutation阶段
。
再进一步,mutation阶段
执行DOM
操作的依据为effectTag
。
所以,对于HostComponent
、ClassComponent
如果包含ref
操作,那么也会赋值相应的effectTag
。
// ...
export const Placement = /* */ 0b0000000000000010;
export const Update = /* */ 0b0000000000000100;
export const Deletion = /* */ 0b0000000000001000;
export const Ref = /* */ 0b0000000010000000;
// ...
你可以在ReactSideEffectTags文件中看到
ref
对应的effectTag
所以,ref
的工作流程可以分为两部分:
render阶段
为含有ref
属性的fiber
添加Ref effectTag
commit阶段
为包含Ref effectTag
的fiber
执行对应操作
render阶段
在render阶段
的beginWork
与completeWork
中有个同名方法markRef
用于为含有ref
属性的fiber
增加Ref effectTag
。
// beginWork的markRef
function markRef(current: Fiber | null, workInProgress: Fiber) {
const ref = workInProgress.ref;
if (
(current === null && ref !== null) ||
(current !== null && current.ref !== ref)
) {
// Schedule a Ref effect
workInProgress.effectTag |= Ref;
}
}
// completeWork的markRef
function markRef(workInProgress: Fiber) {
workInProgress.effectTag |= Ref;
}
在beginWork
中,如下两处调用了markRef
:
updateClassComponent
内的finishClassComponent,对应ClassComponent
注意ClassComponent
即使shouldComponentUpdate
为false
该组件也会调用markRef
- updateHostComponent,对应
HostComponent
在completeWork
中,如下两处调用了markRef
:
completeWork
中的HostComponent类型completeWork
中的ScopeComponent类型
ScopeComponent
是一种用于管理focus
的测试特性,详见PR
总结下组件
对应fiber
被赋值Ref effectTag
需要满足的条件:
fiber
类型为HostComponent
、ClassComponent
、ScopeComponent
(这种情况我们不讨论)- 对于
mount
,workInProgress.ref !== null
,即存在ref
属性 - 对于
update
,current.ref !== workInProgress.ref
,即ref
属性改变
commit阶段
在commit阶段
的mutation阶段
中,对于ref
属性改变的情况,需要先移除之前的ref
。
function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
// ...
if (effectTag & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
// 移除之前的ref
commitDetachRef(current);
}
}
// ...
}
// ...
你可以在这里看到这段代码
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
if (typeof currentRef === 'function') {
// function类型ref,调用他,传参为null
currentRef(null);
} else {
// 对象类型ref,current赋值为null
currentRef.current = null;
}
}
}
接下来,在mutation阶段
,对于Deletion effectTag
的fiber
(对应需要删除的DOM节点
),需要递归他的子树,对子孙fiber
的ref
执行类似commitDetachRef
的操作。
在mutation阶段一节我们讲到
对于
Deletion effectTag
的fiber
,会执行commitDeletion
。
在commitDeletion
——unmountHostComponents
——commitUnmount
——ClassComponent | HostComponent
类型case
中调用的safelyDetachRef
方法负责执行类似commitDetachRef
的操作。
function safelyDetachRef(current: Fiber) {
const ref = current.ref;
if (ref !== null) {
if (typeof ref === 'function') {
try {
ref(null);
} catch (refError) {
captureCommitPhaseError(current, refError);
}
} else {
ref.current = null;
}
}
}
你可以在这里看到这段代码
接下来进入ref
的赋值阶段。我们在Layout阶段一节讲到
commitLayoutEffect
会执行commitAttachRef
(赋值ref
)
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
// 获取ref属性对应的Component实例
const instance = finishedWork.stateNode;
let instanceToUse;
switch (finishedWork.tag) {
case HostComponent:
instanceToUse = getPublicInstance(instance);
break;
default:
instanceToUse = instance;
}
// 赋值ref
if (typeof ref === 'function') {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}
}
至此,ref
的工作流程完毕。
总结
本节我们学习了ref
的工作流程。
- 对于
FunctionComponent
,useRef
负责创建并返回对应的ref
。 - 对于赋值了
ref
属性的HostComponent
与ClassComponent
,会在render阶段
经历赋值Ref effectTag
,在commit阶段
执行对应ref
操作。