从 React 源码彻底搞懂 Ref 的全部 api

news2025/8/3 6:42:17

ref 是 React 里常用的特性,我们会用它来拿到 dom 的引用。

它一般是这么用的:

函数组件里用 useRef:

import React, { useRef, useEffect } from "react";

export default function App() {
  const inputRef = useRef();

  useEffect(()=> {
    inputRef.current.focus();
  }, []);

  return <input ref={inputRef} type="text" />
}
复制代码

class 组件里用 createRef:

import React from "react";

export default class App  extends React.Component{
  constructor() {
    super();
    this.inputRef = React.createRef();
  }

  componentDidMount() {
    this.inputRef.current.focus();
  }

  render() {
    return <input ref={this.inputRef} type="text" />
  }
}
复制代码

如果想转发 ref 给父组件,可以用 forwardRef:

import React, { useRef, forwardRef, useImperativeHandle, useEffect } from "react";

const ForwardRefMyInput = forwardRef(
  function(props, ref) {
    return <input {...props} ref={ref} type="text" />
  }
)

export default function App() {
  const inputRef = useRef();

  useEffect(() => {
    inputRef.current.focus();
  }, [])

  return (
    <div className="App">
      <ForwardRefMyInput ref={inputRef} />
    </div>
  );
}
复制代码

而且还可以使用 useImperativeHandle 自定义传给父元素的 ref:

import React, { useRef, forwardRef, useImperativeHandle, useEffect } from "react";

const ForwardRefMyInput = forwardRef(
  function(props, ref) {
    const inputRef = useRef();

    useImperativeHandle(ref, () => {
      return {
        aaa() {
          inputRef.current.focus();
        }
      }
    });

    return <input {...props} ref={inputRef} type="text" />
  }
)

export default function App() {
  const inputRef = useRef();

  useEffect(() => {
    inputRef.current.aaa();
  }, [])

  return (
    <div className="App">
      <ForwardRefMyInput ref={inputRef} />
    </div>
  );
}
复制代码

这就是我们平时用到的所有的 ref api 了。

小结一下:

  • 函数组件里用 useRef 创建 ref 变量,然后原生标签加个 ref 属性指向它
  • 类组件里用 createRef 创建 ref 变量,保存到 this,然后原生标签加个 ref 属性指向它
  • 子组件的 ref 传递给父组件,使用 forwarRef 包裹子组件,然后原生标签加个 ref 属性指向传进来的 ref 参数。
  • 改变 ref 传递的值,使用 useImperativeHandle,第一个参数是 ref,第二个参数是返回 ref 值的函数

相信开发 React 项目,大家或多或少会用到这些 api。

那这些 ref api 的实现原理是什么呢?

下面我们就从源码来探究下:

我们通过 jsx 写的代码,最终会编译成 React.createElement 等 render function,执行之后产生 vdom:

所谓的 vdom 就是这样的节点对象:

vdom 是一个 children 属性连接起来的树。

react 会先把它转成 fiber 链表:

vdom 树转 fiber 链表树的过程就叫做 reconcile,这个阶段叫 render。

render 阶段会从根组件开始 reconcile,根据不同的类型做不同的处理,拿到渲染的结果之后再进行 reconcileChildren,这个过程叫做 beginWork:

比如函数组件渲染完产生的 vom 会继续 renconcileChildren:

beginWork 只负责渲染组件,然后继续渲染 children,一层层的递归。

全部渲染完之后,会递归回来,这个阶段会调用 completeWork:

这个阶段会创建需要的 dom,然后记录增删改的 tag,同时也记录下需要执行的其他副作用到 effect 链表里。

之后 commit 阶段才会遍历 effect 链表根据 tag 来执行增删改 dom 等 effect。

commit 阶段也分了三个小阶段,beforeMutation、mutation、layout:

在源码里就是并排的 3 个 do while 循环:

它们都是消费的同一条 effect 链表,但是每个阶段做的事情不同,所以上图里有 nextEffect = fistEffect 这一行,也就是每个阶段处理完了,就让下个阶段从头开始处理 effect。

mutation 阶段会根据标记增删改 dom,也就是这样的:

所以这个阶段叫做 mutation,它之前的一个阶段叫做 beforeMutation,而它之后的阶段叫做 layout。

小结下 react 的流程:

通过 jsx 写的代码会编译成 render function,执行产生 vdom,也就是 React Element 对象的树。

react 分为 render 和 commit 两个阶段:

render 阶段会递归做 vdom 转 fiber,beginWork 里递归进行 reconcile、reconcileChildren,completeWork 里创建 dom,记录增删改等 tag 和其他 effect

commit 阶段遍历 effect 链表,做三轮处理,这三轮分别叫做 before mutation、mutation、layout,mutation 阶段会根据 tag 做 dom 增删改。

ref 的实现同样是在这个流程里的。

首先,我们 ref 属性肯定是加在原生标签上的,比如 input、div、p 这些,所以只要看 HostComponent 的分支就可以了,HostComponent 就是原生标签。

可以看到处理原生标签的 fiber 节点时,beginWork 里会走到这个分支:

里面调用 markRef 打了个标记:

前面说的 tag 就是指这个 flags。

在 completeWork 里,判断 flags 如果不是默认的,那就把这个 fiber 记录到父节点的 firstEffect -> nextEffect -> nextEffect 这样的链表里:

这里记录到的是父组件的 effect 链表,那父组件又会记录到它的父组件里,这样最终就在 root fiber 里记录了完整的 effect 链表。

然后就到了 commit 阶段,开始处理这条 effect 链表:

你可以看到在 mutation 阶段,操作 dom 之前,如果有 ref 标记,也就是会用到 ref,那就会 dettachRef 清空 ref。

之后在 layout 阶段,这时候已经操作完 dom 了,就设置新的 ref:

ref 的元素就是在 fiber.stateNode 属性上保存的在 render 阶段就创建好了的 dom,:

这样,在代码里的 ref.current 就能拿到这个元素了:

而且我们可以发现,他只是对 ref.current 做了赋值,并不管你是用 createRef 创建的、useRef 创建的,还是自己创建的一个普通对象。

我们试验一下:

我创建了一个普通对象,current 属性依然被赋值为 input 元素。

那我们用 createRef、useRef 的意义是啥呢?

看下源码就知道了:

createRef 也是创建了一个这样的对象,只不过 Object.seal 了,不能增删属性。

用自己创建的对象其实也没啥问题。

那 useRef 呢?

useRef 也是一样的,只不过是保存在了 fiber 节点 hook 链表元素的 memoizedState 属性上。

只是保存位置的不同,没啥很大的区别。

同样,用 forwardRef 转发的 ref 也很容易理解,只是保存的位置变了,变成了从父组件传过来的 ref:

那 forwardRef 是怎么实现这个 ref 转发的呢?

我们再看下源码:

forwarRef 函数其实就是创建了个专门的 React Element 类型:

然后 beginWork 处理到这个类型的节点会做专门的处理:

也就是把它的 ref 传递给函数组件:

渲染函数组件的时候专门留了个后门来传第二个参数:

所以函数组件里就可以拿到 ref 参数了:

这样就完成了 ref 从父组件到子组件的传递:

那 useImperativeHandle 是怎么实现的修改 ref 的值呢?

源码里可以看到 useImperativeHandle 底层就是 useEffect,只不过是回调函数是把传入的 ref 和 create 函数给 bind 到 imperativeHandleEffect 这个函数了:

而这个函数里就是更新 ref.current 的逻辑:

在 layout 阶段会调用所有的生命周期函数,比如 class 组件的生命周期和 function 组件的 effect hook 的回调:

这里就调用了 useImperativeHandle 的回调:

更新了 ref 的值:

hook 的 effect 和前面的处理 ref 的 effect 保存在不同的地方:

增删改 dom、处理 ref 等这些 effect 是在 fistEffect、lastEffect、nextEffect 的链表里:

而 hook 的 effect 保存在 updateQueue 里:

小结下 ref 的实现原理:

beginWork 处理到原生标签也就是 HostComponent 类型的时候,如果有 ref 属性会在 flags 里加一个标记。

completeWork 处理 fiber 节点的时候,flags 不是默认值的 fiber 节点会被记录到 effect 链表里,通过 firstEffect、lastEffefct、nextEffect 来记录这条链表。

commit 阶段会处理 effect 链表,在 mutation 阶段操作 dom 之前会清空 ref,在 layout 阶段会设置 ref,也就是把 fiber.stateNode 赋值给 ref.current。

react 并不关心 ref 是哪里创建的,用 createRef、useRef 创建的,或者 forwardRef 传过来的都行,甚至普通对象也可以,createRef、useRef 只是把普通对象 Object.seal 了一下。

forwarRef 是创建了单独的 vdom 类型,在 beginWork 处理到它的时候做了特殊处理,也就是把它的 ref 作为第二个参数传递给了函数组件,这就是它 ref 转发的原理。

useImperativeHandle 的底层实现就是 useEffect,只不过执行的函数是它指定的,bind 了传入的 ref 和 create 函数,这样在 layout 阶段调用 hook 的 effect 函数的时候就可以更新 ref 了。

总结

我们平时会用到 createRef、useRef、forwardRef、useImperativeHandle 这些 api,而理解它们的原理需要熟悉 react 的运行流程,也就是 render(beginWork、completeWork) + commit(before mutation、mutation、layout)的流程。

从底层原理来说,更新 ref 有两种方式:

  • useImperativeHandle 通过 hook 的流程更新
  • ref 属性通过 effect 的方式更新

这两种 effect 保存的位置不一样,ref 的 effect 是记录在 fistEffect、nextEffect、lastEffect 链表里的,而 hooks 的 effect 是记录在 updateQueue 里的。

理解了 react 运行流程,包括普通 effect 的流程和 hook 的 effect 的流程,就能彻底理解 React ref 的实现原理。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/15931.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

几乎涵盖了近半年90%的Java面试题,可以肝起来了

前言 很多人在问&#xff1a;八股文还有必要背吗&#xff1f; 近半年来大家听到的、用到的不少&#xff0c;带来的争议也不断。 有人奉为面试神器&#xff0c;全文背诵。有人觉得八股文铺天盖地实际作用不大&#xff0c;还害人不浅… 我觉得不是背不背八股文的问题&#xff0c…

【机器学习并行计算】2 parameter server参数服务器

使用ps实现异步梯度下降。 14年提出的。 异步 vs 同步 可以看出异步运行效率非常高。 异步梯度下降的流程 ps架构流程 worker&#xff1a; 首先从参数服务器拉取最新的参数&#xff1b;然后用自己节点上的数据计算梯度&#xff1b;最后把梯度推给参数服务器参数服务器&#xf…

最快速的文件传输软件,解析镭速文件传输软件

想到每天都需要进行文件传输&#xff0c;就会烦躁&#xff0c;要是有一夸最快速的文件传输软件的话&#xff0c;这样就可以节省大量的时间了&#xff0c;那么针对于用户的这一个需求&#xff0c;我们来介绍一下镭速的文件传输软件&#xff0c;看是否是那么快&#xff0c;快到你…

无监督端到端框架:IVIF

VIF-Net: An Unsupervised Framework for Infrared and Visible Image Fusion &#xff08; VIF-Net: 红外和可见光图像融合的无监督框架&#xff09; &#xff08;本文理解上的难易程度&#xff1a;易&#xff09; 在本文中&#xff0c;我们提出了一种用于红外和可见图像融合…

css 动画实现节流效果

今天在做节流操作时&#xff0c;无意间看到可以用css动画去实现节流效果&#xff0c;然后一顿操作发现果然可以&#xff0c;记录一下 CSS pointer-events 属性 一、 用css中的pointer-events&#xff08;指针事件&#xff09;、animation&#xff08;动画&#xff09;以及:act…

WordPress做缓存Memcached Is Your Friend+Batcache

宝塔面板有两个地方有Memcached,一个是在软件商店的运行环境里面,一个是在php扩展里面,我们先安装PHP扩展中的Memcached ,然后wp后台搜索Memcached Is Your Friend安装插件。WordPress做缓存很给力。缓存命中率保持在 90%以上的WordPress 本地缓存加速方案。 默认我们看到命…

E-Payment Integrator Delphi Edition

E-Payment Integrator Delphi Edition 通过为组件提供处理信用卡和电子支票交易的直观界面&#xff0c;减轻了集成电子支付支持的复杂性。开发人员无需学习复杂的套接字编程或安全实现。通过使用电子支付集成器&#xff0c;开发人员能够针对当前支持的任何支付网关进行定位和开…

比较复杂的策略路由综合实验

下面是网络技能大赛策略这个模块的要求&#xff0c;单独拿出来整理一下 R1、R2、R3间运行OSPF&#xff0c;进程号20&#xff0c;规划单区域&#xff1a;区域0&#xff1b; VSU、R2、R3间运行OSPF&#xff0c;进程号21&#xff0c;规划单区域&#xff1a;区域0&#xff1b; …

JavaScript之事件高级(53rd)

1、注册事件(绑定事件) 给元素添加事件&#xff0c;称为注册事件或者绑定事件。 注册事件有两种方式&#xff1a;传统方式和方法监听注册方式 1、addEventListener事件监听方式 1、eventTarget.addEventListener()方法将指定的监听器注册到 eventTarget&#xff08;目标对…

springMVC异常处理的知识点+异常处理案例

springMVC异常处理的知识点异常处理案例 异常介绍&#xff1a; 我们知道系统中的异常包括两类&#xff1a;预期异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息&#xff0c;后者主要通过规范代码开发、测试等手段减少运行时异常的发生 在SpringMVC处理异…

有关服务器虚拟化的常见问题解答

虚拟化”一词经常使用&#xff0c;尤其是与服务器相关的时候。以下是一些有关服务器虚拟化常见问题的解答。 什么是服务器虚拟化? 虚拟化是一个经常应用于范围广泛的技术的术语。从本质上讲&#xff0c;虚拟化技术就是使用分布式软件硬件。在服务器虚拟化领域&#xff0c;这意…

Android

直接运行 最新版的 apktool 可以通过brew安装&#xff0c;命令如下 brew install apktool # 验证安装结果apktool -version (Mac)反编译Android APK详细操作指南[ApkTool,dex2jar,JD-GUI] - CrazyCodeBoy的技术博客官网|CrazyCodeBoy|Devio|专注移动技术开发(Android&I…

【计算机网络】广域网协议分析

注&#xff1a;最后有面试挑战&#xff0c;看看自己掌握了吗 文章目录广域网WAN广域网局域网区别PPP协议----链路层------广域网PPP协议组成3个ppp状态图PPP协议帧格式HDLC协议--------链路层------广域网帧格式------无监信PPP协议和HDLC&#x1f343;博主昵称&#xff1a;一拳…

通用Mapper获取数据表中id为0解决方法。千万别瞎改int为integer了

项目场景&#xff1a; 最近准备自己写一个框架。由于是舍弃了成熟框架&#xff0c;所以在集成一些组件的时候&#xff0c;发现了一些之前没有注意过的问题。 这次是集成通用mapper时出现的一个问题。。。 问题描述 使用通用Mapper的selectAll()方法后&#xff0c;得到的id值都…

Java【抽象类和接口】是什么?

文章目录前言一、抽象类1.什么是抽象类2.抽象类的特征3.抽象类的作用二、接口1.什么是接口2.语法规范3.接口的使用4.接口的特征5.实现多个接口6.接口间的继承总结前言 前篇文章分享过了 面向对象语言的三大特征&#xff1a;封装&#xff0c;继承&#xff0c;多态 本篇呢将继续…

LCR表测电容怎么实现连续测试?ATECLOUD云测试平台帮您解决

河南某企业在生产测试中需要用到LCR表测试电容的容量和损耗&#xff0c;目前在使用是德科技E4980a LCR 测试数据时&#xff0c;发现不能连续测试&#xff0c;不能自动记录数据。通过和纳米软件Namisoft沟通&#xff0c;希望我们可以实现连续测试并将测试数据自动记录&#xff0…

steam搬砖项目,当下最稳定长久的

我为什么这么推荐普通人来做CSGO搬砖&#xff0c;最主要的原因就两点&#xff0c;一是平台流量大知道的人少&#xff0c;二就是不需要什么成本&#xff01; CSGO是基于steam衍生出来的&#xff0c;steam的流量池有多大&#xff0c;想必大家都心知肚明&#xff0c; Steam平台是…

实验(一):开发环境的安装及基本操作

一、实验目的与任务 实验目的&#xff1a; 1. 掌握KeilC集成开发环境的安装及基本操作&#xff1b; 2. 掌握Protues仿真软件的安装及基本操作。 任务&#xff1a; 1.完成KeilC集成开发环境的安装&#xff0c;建立工程&#xff1b; 2. 完成Protues仿真软件的安装&#xff0c;建立…

[附源码]java毕业设计票务销售网站

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Gartner发布首份中国RPA魔力象限报告,弘玑Cyclone被重点推荐

11月17日&#xff0c;国际权威研究机构Gartner重磅发布首份中国RPA魔力象限报告&#xff1a;《China Context: Magic Quadrant for Robotic Process Automation》。 毫无悬念&#xff0c;在2022全球RPA魔力象限报告&#xff08;Gartner RPA MQ&#xff09;中表现优异的弘玑Cyc…