事实上,任何需要被”引用”的数据都可以保存在ref
中,useRef
的出现将这种思想进一步发扬光大。
在我们讲到:
本节我们会介绍useRef
的实现,以及ref
的工作流程。
由于string
类型的ref
已不推荐使用,所以本节针对function | {current: any}
类型的ref
。
与其他Hook
一样,对于mount
与update
,useRef
对应两个不同dispatcher
。
你可以在看到这段代码
可见,useRef
仅仅是返回一个包含current
属性的对象。
为了验证这个观点,我们再看下React.createRef
方法的实现:
export function createRef(): RefObject {
const refObject = {
current: null,
};
return refObject;
}
你可以从这里 (opens new window)看到这段代码
了解了ref
的数据结构后,我们再来看看ref
的工作流程。
在React
中,HostComponent
、ClassComponent
、ForwardRef
可以赋值ref
属性。
// HostComponent
<div ref={domRef}></div>
// ClassComponent / ForwardRef
<App ref={cpnRef} />
其中,ForwardRef
只是将ref
作为第二个参数传递下去,不会进入ref
的工作流程。
所以接下来讨论ref
的工作流程时会排除ForwardRef
。
所以,对应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文件 (opens new window)中看到
ref
对应的effectTag
所以,ref
的工作流程可以分为两部分:
render阶段
为含有ref
属性的fiber
添加Ref effectTag
commit阶段
为包含Ref effectTag
的fiber
执行对应操作
在render阶段
的beginWork
与completeWork
中有个同名方法markRef
用于为含有ref
属性的fiber
增加Ref effectTag
。
// beginWork的markRef
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;
}
你可以在这里 (opens new window)看到
beginWork
的markRef
、看到completeWork
的markRef
在beginWork
中,如下两处调用了markRef
:
updateClassComponent
内的finishClassComponent (opens new window),对应ClassComponent
注意ClassComponent
即使shouldComponentUpdate
为false
该组件也会调用markRef
- ,对应
HostComponent
在completeWork
中,如下两处调用了markRef
:
completeWork
中的HostComponent (opens new window)类型completeWork
中的类型
fiber
类型为HostComponent
、ClassComponent
、ScopeComponent
(这种情况我们不讨论)对于
mount
,workInProgress.ref !== null
,即存在ref
属性对于
update
,current.ref !== workInProgress.ref
,即属性改变
在commit阶段
的mutation阶段
中,对于ref
属性改变的情况,需要先移除之前的ref
。
你可以在看到这段代码
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
// 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
的赋值阶段。我们在讲到
commitLayoutEffect
会执行commitAttachRef
(赋值ref
)
至此,ref
的工作流程完毕。
本节我们学习了ref
的工作流程。
对于
FunctionComponent
,useRef
负责创建并返回对应的ref
。