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 hookconst hook = mountWorkInProgressHook();// 创建refconst ref = {current: initialValue};hook.memoizedState = ref;return ref;}function updateRef<T>(initialValue: T): {|current: T|} {// 获取当前useRef hookconst hook = updateWorkInProgressHook();// 返回保存的数据return hook.memoizedState;}
你可以在这里
(opens new window)看到这段代码
可见,useRef仅仅是返回一个包含current属性的对象。
为了验证这个观点,我们再看下React.createRef方法的实现:
export function createRef(): RefObject {const refObject = {current: null,};return refObject;}
你可以从这里
(opens new window)看到这段代码
了解了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为传递下去的reflet children = Component(props, secondArg);
你可以在这里
(opens new window)看到这段代码
我们知道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文件
(opens new window)中看到
ref对应的effectTag
所以,ref的工作流程可以分为两部分:
render阶段为含有ref属性的fiber添加Ref effectTagcommit阶段为包含Ref effectTag的fiber执行对应操作
render阶段
在render阶段的beginWork与completeWork中有个同名方法markRef用于为含有ref属性的fiber增加Ref effectTag。
// beginWork的markReffunction markRef(current: Fiber | null, workInProgress: Fiber) {const ref = workInProgress.ref;if ((current === null && ref !== null) ||(current !== null && current.ref !== ref)) {// Schedule a Ref effectworkInProgress.effectTag |= Ref;}}// completeWork的markReffunction markRef(workInProgress: Fiber) {workInProgress.effectTag |= Ref;}
你可以在这里
(opens new window)看到
beginWork的markRef、这里(opens new window)看到
completeWork的markRef
在beginWork中,如下两处调用了markRef:
updateClassComponent内的finishClassComponent
(opens new window),对应ClassComponent
注意ClassComponent即使shouldComponentUpdate为false该组件也会调用markRef
- updateHostComponent
(opens new window),对应HostComponent
在completeWork中,如下两处调用了markRef:
completeWork中的HostComponent
(opens new window)类型completeWork中的ScopeComponent
(opens new window)类型
ScopeComponent是一种用于管理focus的测试特性,详见PR(opens new window)
总结下组件对应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) {// 移除之前的refcommitDetachRef(current);}}// ...}// ...
你可以在这里
(opens new window)看到这段代码
function commitDetachRef(current: Fiber) {const currentRef = current.ref;if (currentRef !== null) {if (typeof currentRef === 'function') {// function类型ref,调用他,传参为nullcurrentRef(null);} else {// 对象类型ref,current赋值为nullcurrentRef.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;}}}
你可以在这里
(opens new window)看到这段代码
接下来进入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;}// 赋值refif (typeof ref === 'function') {ref(instanceToUse);} else {ref.current = instanceToUse;}}}
至此,ref的工作流程完毕。
总结
本节我们学习了ref的工作流程。
对于
FunctionComponent,useRef负责创建并返回对应的ref。对于赋值了
ref属性的HostComponent与ClassComponent,会在render阶段经历赋值Ref effectTag,在commit阶段执行对应ref操作。
