05|深入理解useRef
基本概念
在 FunctionComponent 中创建 ref 需要使用到useRef
这个hook,函数签名如下
const refContainer = useRef(initalValue)
useRef 函数接收一个初始值,并返回一个可变的(mutable) ref 对象,也就是我们的变量refContainer
。这个对象上面会存在一个属性——current
,它的默认值就是initalValue
。这里有个比较重要的点就是useRef 创建返回的对象是可变的,并且它会在组件的整个生命周期中都存在,这也就以为着无论你在何时何地访问这个对象,它的值永远都是最新的值。
useState 创建出来的值是不可变的(immutable),每一次 render 都是新的值,而 useRef 创建出来的对象始终都是最开始的那个对象,其属性
current
可以被赋值。
被 state 的数据更新所折磨的你是不是心里乐开了花儿 🤪 🤪 🤪
能替代useState吗?
不过不要高兴的太早,我们来看下它能不能代替“烦人”的 useState
function App() {
const count = useRef(0)
const addCount = () => {
count.current++
}
const [num, setNum] = useState(0)
const addNum = () => {
setNum(num + 1)
}
const logCount = () => {
console.log(count.current)
}
return (
<div>
<div>
<span>count:{count.current}</span>
<span>num:{num}</span>
</div>
<button onClick={addCount}>count+1</button>
<button onClick={logCount}>查看count</button>
<button onClick={addNum}>更新num,重新render</button>
</div>
)
}
效果如下👇
可以看到,尽管它的数值的确被改变了,但是它无法让我们的dom更新,也许你会觉得很奇怪,为什么数据改变了但是没有触发 render 呢?
要知道,我们使用 useState 都需要使用它提供的 dispatch (useState返回一个数组,第一个值是数据,第二个值是用来修改数据的方法,也就是dispatch)去触发函数组件 render 的,所以我们这里的 useRef 返回的这个对象与下面这样创建的对象从本质上来讲没有什么区别。
const obj = { current: 0 }
useRef 并不能替代掉 useState,因为它只是一个普通的变量,无法在更新数据的同时让视图同步更新。
useRef的本质是引用
注意:下面所有写到“函数组件(fiber)”其实都应该是fiber,如果你不理解fiber,那么就当它是函数组件也没关系
我们来透过源码来看useRef,会发现 useRef 创建的对象会存放到对应的函数组件(fiber)上,源码如下👇
当我们首次执行useRef(initialValue)
时最终会执行mountRef
,这个函数干如下五件事:
- 接收
initialValue
- 在
mountWorkInProgressHook
中创建一个hook对象,并将其挂载到当前这个函数组件(fiber对象)上 - 创建一个 ref 对象的变量,将
initialValue
赋值到current
属性上 - 将这个 ref 变量挂载到hook上(即挂在到组件上)
- 返回 ref
其中第四步也就将这个数据与组件关联起来了,所以这个数据不会被丢失,并且你能在任何地方访问到最新的ref的值就是这个原因
当再次render执行useRef
就更加简单了
从函数组件(fiber)上拿到当前这个 hook,并返回这个对象。
所以useRef
利用的就是 js 中的引用,将对象存储在函数组件(fiber)上,使得在任何时间任何地点都能访问到最新的值。
ref 的应用
ref 主要有以下3种应用方式
- 数据存储
- 获取 dom 对象
- 组件通讯
数据存储
数据存储一般用于与dom不相关的数据,比如我们做计时器类带需求时,可以用来存储定时器指针👇
function App() {
const timer = useRef(null)
const [num, setNum] = useState(0)
const start = () => {
timer.current = setInterval(() => {
// 设置新的状态值时记得这样写~
setNum(num => num + 1)
}, 1000)
}
const clear = () => {
clearInterval(timer.current)
}
useEffect(() => {
// 组件销毁记得清除定时器
return () => {
clear()
}
}, [])
return (
<div>
<div>{num}</div>
<div>
<button onClick={start}>start</button>
<button onClick={clear}>clear</button>
</div>
</div>
)
}
获取dom对象
function App() {
const domRef = useRef()
const handler = () => {
console.log(domRef.current)
}
return (
<div ref={domRef}>
<button onClick={handler}>查看domRef</button>
</div>
)
}
组件通讯
ref在react中也可以充当一种通讯方式,比如父组件获取子组件的dom、数据或方法。
- 只获取子组件的dom
function Parent() {
const childRef = useRef<HTMLDivElement>(null)
const handler = () => {
// 父组件这里就能获取到子组件内的dom了
console.log(childRef.current)
}
return (
<div>
<button onClick={handler}>获取子组件</button>
<RefChild ref={childRef} />
</div>
)
}
// 子组件接收 ref 参数
const Child: React.ForwardRefRenderFunction<HTMLDivElement, any> = (props, ref) => {
return (
// 将ref参数传递到原生组件上
<div ref={ref}>child</div>
)
}
// 经过 forwardRef 转发 ref
const RefChild = forwardRef(Child)
- 父组件获取子组件内到数据或方法,需要子组件中配合
useImperativeHandle
暴露出对应的数据与方法
function Parent() {
const childRef = useRef<RefType>({
num: 1,
add: () => {}
})
const handler = () => {
console.log(childRef.current)
// { num: 1, add: Function }
}
return (
<div>
<button onClick={handler}>获取子组件数据</button>
<RefChild ref={childRef} />
</div>
)
}
const Child: React.ForwardRefRenderFunction<RefType, any> = (props, ref) => {
const [num, setNum] = useState(1)
const add = () => {
setNum(num + 1)
}
useImperativeHandle(ref, () => {
return {
num,
add
}
})
return <div onClick={add}>{num}</div>
}
const RefChild = forwardRef(Child)
interface RefType {
num: number,
add: () => void
}
面试题
这是一道真实的面试题,题目如下
进入页面开始每秒发送一个请求,请求需携带定义的 state 值(最新的),仅可触发一次 setInterval 。
我们针对题目进行分析如下:
- 在进入页面时进行请求,必然需要使用
**useEffect**
- 仅允许触发一次 setInterval,则说明该
**useEffect**
的依赖项为空数组 - 在每次请求时都需要保持最新的 state,但依赖项又需要为空数组,说明要使用到
**useRef**
来获得最新的值。
实现代码如下👇
function App() {
const [state, setState] = useState(0)
const getLastState = () => state
const queryHandlerRef = useRef(getLastState)
// 每次render都更新为最新的函数
queryHandlerRef.current = getLastState
useEffect(() => {
const tick = () => {
// 假设这里会使用 state 发送请求
const s = queryHandlerRef.current()
console.log(s)
}
let timer = setInterval(tick, 1000)
return () => clearInterval(timer)
}, [])
return (
<div>
<div>{state}</div>
<button onClick={() => setState(state + 1)}>state + 1</button>
</div>
)
}
效果如下👇
尽管我们满足了要求,但是代码耦合严重,我们可以将代码封装成一个自定义hook
function useInterval(callback: Function, delay: number) {
const savedCallback = useRef(callback);
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
总结
OK,这一节我们结合useState,useEffect来讲useRef。主要讲了如下内容
- 介绍了useRef的使用场景
- 跨组件需要配合
forwardRef
来传递ref
- 父组件想要调用子组件内部的数据需要子组件通过
useImperativeHandle
来暴露。 - 通过分析一个面试题来解决问题,最后还将其封装成了一个自定义hook
转载自:https://juejin.cn/post/7123193200361570312