欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 美景 > React 源码揭秘 | Ref更新原理

React 源码揭秘 | Ref更新原理

2025/5/10 4:26:10 来源:https://blog.csdn.net/weixin_40710412/article/details/145898657  浏览:    关键词:React 源码揭秘 | Ref更新原理

Ref主要作用于HostComponent组件,用来获取其真实的DOM节点。

除此以外,我们也通常用useRef等hooks来维护一个稳定的值,并且在该值改变的时候不触发更新

Ref 结构

Ref的结构一般为 Ref: { current: 你要保存的值 }

多套一层current保存的原因主要是,为了保证其保存值的稳定。

比如,对于函数组件来说,每次更新都会重新渲染执行函数,如果用ref直接保存变量 如

const myRef = useRef({a:100}) // 假设 myRef 直接就是对象 { a: 100 }

那么,当我们修改myRef时,比如

myRef = { b: 200 }

那么此时修改的,其实是当前函数作用域下myRef变量的指向,也就是说吧myRef指向了另外一个对象,对useRef中保存的Ref对象没有任何影响,那么在下次Update时,这个新的Ref值就会丢失!

 所以采用修改一个对象内current属性的方式,保持值的稳定。

Ref的使用

Ref的使用方式一般有两种  

  1. ref = {ref} 直接传入ref属性
  2. ref = {dom=>ref.current=dom} 传入函数进行赋值

我们通常使用useRef() 获取Ref,但是其实,其本质就是创建了一个包含current属性的对象而已,如果你能保证这个对象在每次更新时的稳定,你甚至可以自己new一个对象作为Ref

useRef 

useRef 用来在函数组件中创建并且维护一个稳定的Ref,其实现非常简单

/** 挂载Ref */
function mountRef<T>(initialValue: T): { current: T } {const hook = mountWorkInProgressHook();hook.memorizedState = { current: initialValue };return hook.memorizedState;
}/** 更新Ref 其实就是保存一个值 */
function updateRef<T>(): { current: T } {const hook = updateWorkInProgressHook();return hook.memorizedState;
}

把你传入的值,封装成 { current: value } 的形式,保存在Hook.memorizedState上,并且在下次调用的时候返回,即可。 

所以,useRef不一定要和dom绑定,我们通常的做法都是将其作为一个可以稳定存储某个值,并且在修改时不触发更新的hook

Ref 更新流程

Ref的本意是,方便获取HostComponent的真实dom元素,或者ClassComponent的实例对象。

函数组件中没有Ref,因为Ref不知道要绑定什么,所以需要使用forwardRef和useImpreciateHa

ndle一同来确定Ref的指向。

在这里我们只讨论HostComponent的Ref更新和卸载。

和action的处理方式类似,Ref在Host的组件的 挂载,卸载,或者Ref对象变化的时候,都会检查。

如果传入的Ref为对象,则

  1. 挂载阶段,直接 ref.current = dom
  2. 卸载阶段 ref.current = null
  3. 更新阶段 先卸载oldRef oldRef.current = null 再更新 新的ref newRef.current = dom

如果传入Ref为函数,则

  1. 挂载阶段 ref(dom)
  2. 卸载阶段 ref(null)
  3. 更新阶段 oldRef(null) newRef(dom)

在调用createElement创建Element元素时,会对传入的props.ref进行特殊处理

/** 实现createElement方法 */
export function createElement(type: ReactElementType,props: ReactElementProps,...children: ReactElementChildren[]
): ReactElement {return {$$typeof: REACT_ELEMENT_TYPE,type,// 特殊处理REF 如果没传入,默认赋nullref: props.ref ? props.ref : null,key: props.key ? String(props.key) : null,props: {...props,/** 源码这里做了处理 如果只有一个child 直接放到children 如果有多个 则children为一个数组 */children:children?.length === 1? handleChildren(children[0]): children.map(handleChildren),},};
}

ref会被单独作为一个字段保存,在通过element创建Fiber的时候, 会默认吧Fiber.ref置null

 

在beginwork阶段,对每个HostComponent元素,都会执行markRef(wip)的操作

/** 处理普通节点的比较 */
function updateHostComponent(wip: FiberNode): FiberNode {/** 1.获取element.children */const hostChildren = wip.pendingProps?.children;// 目前只有在HostComponent中标记RefmarkRef(wip);/** 2. 协调子元素 */reconcileChildren(wip, hostChildren);/** 3.返回第一个child */return wip.child;
}

markRef的逻辑很简单,会根据不同阶段判断

  • 如果是挂载阶段,如果ref不为null,就给当前HostComponent的fiber对象打上Ref标记,在commit阶段会处理。
  • 如果是在更新阶段,如果本次更新的Ref不等于currentFiber上的Ref 说明更新变动了Ref,需要给Fiber打Ref标记。

实现如下

Ref的变动 只有在

1. 挂载

2. 卸载

3. Ref对象变动

其中 卸载不需要标记Ref 只有挂载和Ref对象变动时才标记

/** 标记Ref [生产Ref] */
function markRef(wip: FiberNode) {const current = wip.alternate;const ref = wip.ref;if (current === null && ref !== null) {// mount阶段 如果wip有ref则绑定flagwip.flags |= Ref;return;}if (current !== null && ref !== current.ref) {// update阶段 wip.ref和current.ref不相等 (useImmpreciatHandle改变ref)需要重新挂载refwip.flags |= Ref;return;}
}

commit阶段,真正完成对Ref对象的卸载和挂载。其中

  • 卸载Ref在Mutation阶段,对有所打Ref标记的Fiber和被删除的Fiber的Ref进行卸载操作,其中完成卸载的函数为saftyDetachRef
  • 挂载Ref在Layout阶段,其中完成挂载Ref的函数为saftyAttachRef 

在commitMutationEffectOnFiber中,有对Ref的判断

 // commitMutationEffectOnFiber// 卸载Ref 只有hostComponent需要卸载if (finishedWork.tag === HostComponent && (flags & Ref) !== NoFlags) {const current = finishedWork.alternate;if (current) {// 需要卸载current的ref 其实本质上current和finishedWork的ref都是一个saftyDetachRef(current);}// 卸载之后由于可能还会加载ref 所以这里的flag不能~Ref}

 在commitDeletion中,也包含了对Ref节点的安全卸载

卸载函数如下:

/** 卸载Ref*  卸载时机 commit的mutation阶段 包括*  1. 组件卸载*  2. 组件更新时包含Ref (Ref变动)*/
function saftyDetachRef(current: FiberNode) {// 这里传入的是current的fiber 也就是旧的fiber 卸载的也是旧的fiber// fiber会在createWorkinprogress复用传递 这里的作用就是 ref.current = null / ref(null)const ref = current.ref;if (ref === null) return;// ref可以是函数或者对象 判读类型if (typeof ref === "function") {// 卸载/更新变动之前卸载时 都会执行下ref函数 并且传入nullref(null);} else {ref.current = null;}
}

其中,会先检测Ref是否存在,如果Ref不存在则直接return

Ref存在 则会根据Ref的类型,执行函数传入null或者直接把对象的current置为null

如果传入的是基本类型,不是对象,由于在对基本类型 . 属性 的时候,会先创建包装类,然后赋值,并且使用之后销毁包装类,所以也不会报错,只是没有任何的效果!

挂载Ref

次阶段在Layout阶段,此时调用saftyAttachRef

/** 附加Ref 附加时机*  commit的layout阶段 也就是真实dom更新完成 渲染之前*/
function saftyAttachRef(finishedWork: FiberNode) {const ref = finishedWork.ref;const dom = finishedWork.stateNode;if (ref !== null) {if (typeof ref === "function") {ref(dom);} else {ref.current = dom;}}
}

和卸载类似,只不过传入dom

其在commitLayEffect中被调用

/** 用来处理 Layout副作用 [Ref] */
const commitLayoutEffectsOnFiber: CommitCallback = (finishedWork) => {// 处理每个节点的Effect// 获取节点的flagsconst flags = finishedWork.flags;if (finishedWork.tag === HostComponent && (flags & Ref) !== NoFlags) {saftyAttachRef(finishedWork);finishedWork.flags &= ~Ref;}
};

所以,在commit的Mutation阶段和Layout阶段都会设置Ref 在layout阶段更新完Ref之后,才将Ref的flag删除!

 

 

 

 

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词