在使用 React 进行开发过程中,或多或少使用过 Refs 进行 DOM 操作或者访问一些DOM上的API,又或使用 Refs 保存数据。不管怎么说 Refs 总是 React 提供的一大助力,这篇文章主要介绍 Refs 功能和使用场景以及注意事项。希望能增强对 Refs 的理解,掌握好这把利剑。
Refs 是 React 提供的用来保存 object 引用的一个解决方案,在函数式组件使用 useRef 创建一个 ref 对象,ref 对象存在一个可直接修改的 current 属性,内容都是存在 current 上。Refs 使用场景主要分为两个方向,其一是实现 DOM 访问与操控、在两次render之间传递数据内容【和state机制有很大不同,下文会有对比介绍】。如果在组件返回的 jsx dom上绑定了 ref 属性,React 在处理 jsx 时会把该dom节点【原生node节点】的引用存储在 ref.current 上。
分为三步:
React.createRef() 创建 )Example one 实现点击按钮 focus input 框
import React, { useRef } from "react";
export default function Comp() {// 第一步:使用 useRef 创建一个 ref 对象 { current: null }const ref = useRef();function handleClick() {// 第三步:访问到 ref 上存的内容,这里是 input 的node节点ref.current.focus();}// 第二步:赋值 refreturn (<>>);
}
Example two 实现数据发送3s内撤回功能:在点击发送后3s内如果点击 “取消发送” 则取消本次发送
简单起见我们按钮不实际发送请求,定时 3s 如果3s内点击了 “取消发送”则取消发送。发送功能用提醒 “已发送”代替,出现“已发送”表示执行了发送。
import React, { useRef, useState } from "react";
export default function CompA() {// 第一步:使用useRef 创建 ref 对象const ref = useRef();const [isSending, setIsSending] = useState(false);function send() {// ...window.alert("消息已发送!");setIsSending(false);}function undo() {// 第三步: 访问存在 ref 上的 timeout ID, 进行定时取消clearTimeout(ref.current);}function handleClickSendBtn() {setIsSending(true);// 第二步: 赋值,将 timeout ID 存在 ref 上ref.current = setTimeout(send, 3000);}function handleClickCancelBtn() {undo();setIsSending(false);}return (<>{isSending && }>);
}
我们通过两个简单 case 演示了一下,DOM 操作 以及用于在两次 re-render 之间传递内容(case 2 传递的内容是 timeout 的ID)。在使用 Refs 的过程中有几点尤其需要注意。
在使用 useRef 进行创建 ref 时可以传递 null、number 、object 等内容也可以传递初始化函数。React 只会保存一次初始值,并把它带到下一次 render 中。因此在 useRef 在创建ref的时候传递重复的内容是不生效的,如果你认为每次都生成一个新的值赋给ref但是React给你的却是第一次传递的值,这可能不符合你的预期。
import React, { useEffect, useRef, useState } from "react";
export function CompB() {// 注意: 这个部分不会每次生成一个新的时间戳,只会采用 mounted 时新建的第一次时间戳。const ref = useRef(+new Date());const [count, setCount] = useState(0);useEffect(() => {console.log(`第${count}渲染时间:`, +new Date());console.log("ref", ref.current);});function handleClick() {// 为了让点击时,更新 state 触发 re-rendersetCount(count + 1);}return (<>>);
}
注意:效果图中初始化的时候会打印两次重复的第0次渲染,是因为 React 在 dev 模式下会执行两遍组件内容,检测组件是否是纯组件。并非代码问题,后续研读源码时会有文章介绍,欢迎关注。
对于 state 来说,直接修改state不会生效。需要使用 useState 给的第二个返回值来进行修改。而 ref 则是可直接修改 current 属性上的内容,并且修改后可以立即取到值。ref存储的实际就是一个引用,因此是可突变的。
import React, { useRef } from 'react';
export function Comp() {const ref = useRef(0);useEffect(()=>{console.log(ref.current); // 0// 突变ref.current = ref.current + 1;console.log(ref.current); // 1});return
}
React 组件的 re-render 的触发一般是【state、props、context】中的出现变化引起的。修改 Ref 的内容不会引起组件的 re-render 因此不能用 ref 去干预 React 生成jsx。换句话说就是不能用在jsx中做渲染或者条件判断,不然可能得到没办法预料的jsx结果。
React 约定 state、props、context 都是一样的就应该输出同样的jsx内容,只要这三个要素不变那么以不同的调用顺序执行组件应该得到同样的结果。要说清楚为什么Ref的读写只能在useEffect和回调函数中,得先铺垫一下React的一些架构知识。
React 架构上分为三个部分【调度器Scheduler、协调器Reconciler、渲染器Renderer】,整体上又是两个阶段【render 阶段,commit阶段】。render 阶段的目的是找出哪些组件需要更新,以及如何更新(这些内容会标记在Fiber节点上)【更新过程可中断可抢占的,高优的更新可抢占优先先执行。这个阶段主要是 Scheduler 负责调度优先级, 协调器负责找出更新的内容并标记好】,commit 阶段的作用用一句话就是【根据 render 阶段标记的结果Fiber上的tag,操作dom,执行 useEffect 以及对应阶段的生命周期函数】。
在 render 阶段会执行组件,如果出现高优更新抢占,那么低优先级的更新在高优更新执行完成后会重新执行一遍【函数式组件也就是个function函数,在函数体中间的执行 ref 写操作会被多次执行】,我们会发现如果ref的赋值操作在这个期间执行了那么组件更新的结果就是不可预期的【未被抢占时ref的结果是1,被抢占1次时是2。这完全是不可预期大的】。而 useEffect 或者回调函数都不是在 render 阶段执行的因此每次更新只执行一次。也就是说ref的读写不能出现在render阶段,就只能写在 useEffect【类组件对应的是生命周期函数,注意不能写在 componentWillxxx 生命周期中,因为 componentWillxxx 生命周期函数执行在 render 阶段】和回调函数中。
// bad
function MyComponent() {// ...// 🚩 Don't write a ref during renderingmyRef.current = 123;// ...// 🚩 Don't read a ref during renderingreturn {myOtherRef.current}
;
}
// good
function MyComponent() {// ...useEffect(() => {// ✅ You can read or write refs in effectsmyRef.current = 123;});// ...function handleClick() {// ✅ You can read or write refs in event handlersdoSomething(myOtherRef.current);}// ...
}
React 默认情况下不允许组件访问其他组件的dom节点,因此关闭了直接 props 传递 ref 标记组件的dom这种操作。得借助 React.forwardRef api 传递 实现这种跨组件的dom操作。
import React, { useEffect, useRef, useState, forwardRef } from "react";
export function ParentComp() {const childInputRef = useRef(null);function handleClick() {childInputRef.current?.focus();}return (<> >);
}
// 使用forwordRef 包裹组件,接受 ref 并转发绑定到对应dom上
const ChildComp = forwardRef((props, ref) => {return ();
});
ref 绑定的dom在离屏或者未挂载时ref.current 值会被修改为 null 。如果在组件中间会进行条件渲染,那么需要处理一下判断逻辑,不然代码可能会抛出异常。另外在父组件引用子组件 dom 的场景也应该增加对 null 的判断。至此 Refs 的要点已经介绍完成。
接下来我们接着聊聊什么情况下使用 Refs 比较好,React 官方把 Refs 定义为逃生通道,就是暗示要谨慎使用。