当前位置: 首页 > article >正文

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 如果没传入,默认赋null
    ref: 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中标记Ref
  markRef(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则绑定flag
    wip.flags |= Ref;
    return;
  }

  if (current !== null && ref !== current.ref) {
    // update阶段 wip.ref和current.ref不相等 (useImmpreciatHandle改变ref)需要重新挂载ref
    wip.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函数 并且传入null
    ref(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
  // 获取节点的flags
  const flags = finishedWork.flags;

  if (finishedWork.tag === HostComponent && (flags & Ref) !== NoFlags) {
    saftyAttachRef(finishedWork);
    finishedWork.flags &= ~Ref;
  }
};

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

 

 

 

 


http://www.kler.cn/a/564888.html

相关文章:

  • [算法]——前缀和(二)
  • 事故02分析报告:慢查询+逻辑耦合导致订单无法生成
  • Lua语言入门(自用)
  • tableau之网络图和弧线图
  • 波导阵列天线 学习笔记11双极化全金属垂直公共馈电平板波导槽阵列天线
  • Lucene硬核解析专题系列(一):Lucene入门与核心概念
  • vue3+ts实现动态下拉选项为图标
  • Java高频面试之SE-23
  • Linux8-互斥锁、信号量
  • Kafka 消息 0 丢失的最佳实践
  • spring-data-mongoDB
  • PostgreSQL 17 发布了!非常稳定的版本
  • Spring Boot 与@Bean注解搭配场景
  • 网络安全复习资料
  • Go语言学习笔记(三)
  • 目标检测YOLO实战应用案例100讲-面向无人机图像的小目标检测
  • JAVA面试常见题_基础部分_mybatis面试题
  • 【MySQL】(1) 数据库基础
  • 从工程师到系统架构设计师
  • 【NestJS系列】安装官方nestjs CLI 工具