React中的useRef
最近学习react
中碰到useRef
碰到了一些疑问,顺手记录下来。
useState
碰到的问题
说到useRef
那么我们先来看看useState
存在的"问题"。
import React, { useState, useEffect } from 'react';
export default function Demo() {
const [like, setLike] = useState(0);
useEffect(() => {});
const handleClick = () => {
setLike(like + 1);
};
const getLikeValue = () => {
setTimeout(() => {
alert(like);
}, 2000);
};
return (
<div>
<button onClick={handleClick}>+</button>
<button>{like} 👍</button>
<button onClick={getLikeValue}>获得like值</button>
</div>
);
}
这是一个非常经典的例子,页面上渲染出三个button
。当我点击+
之后,页面重新渲染为1
。
此时当我点击获得Like值
按钮,因为定时器的原因并不会立即进行alert
,此时我在点击+
修改like
。
当两秒过后,你会发现页面上展示的最新的like
值,而alert
弹出的like
停留到了1
。
先放出来关于这段简单代码带来的结论:
当Demo函数每次运行我们都称他为每一次渲染,每一次渲染函数内部都拥有自己独立的props
和state
,当在jsx
中调用代码中的state
进行渲染时,每一次渲染都会获得各自渲染作用域内的props
和state
。
当然这不是人话,咱们接着往下看。
对比vue
更新原理差异
实质上这里和vue
实现响应式的原理是完全不同的,我们都知道在vue3
中是通过proxy
,当修改响应式值的时候会触发对应的set
陷阱函数从而触发更新运行对应收集的effect
进行模版更新。
而在react
这里state
中的like
仅仅是渲染函数中一个定义的数字而已。它并不是什么proxy
,watcher
,effect
...它仅仅是表示一个数字而已。
当我们第一次调用函数,like
赋予初始化值0
。当我们点击按钮调用setLike
,react
会再次渲染组件(运行Demo函数)。此时新函数内部的like
是1
,然后使用内部这个值重新调用Demo
函数进行页面渲染。如此类推,就好比下方这段代码:
const like = 2 // Final value
// ...
<p>{like}</p>
// ...
// During first render
function Counter () {
const like = 0
// ...
<p>{like}</p>
// ...
}
// After a Click, our function is called again
function Counter () {
const like = 1
// ...
<p>{like}</p>
// ...
}
// After another click, our function is called again
function Counter () {
const like = 2
// ...
<p>{like}</p>
// ...
}
// ...
结论分析
当我们每次更新状态的时候,(修改state
值)。react
会重新渲染组件,每一次渲染都可以拿到独立的like
状态,这个状态值是独立于每次渲染函数中的一个常量,它的作用仅仅只是渲染输出,插入jsx
中的数字而已。
你可以会疑惑每次调用函数的like
值是哪里来的,新的like
值是由react
提供,当我们调用setLike
修改它的值的时候。react
会带着新的值去重新运行函数进行再次渲染,保证渲染和输出一致。
这里有一个关键点,任意一次渲染周期(函数调用)的state/prop
(直观来说就是like
值)都不会随着时间改变,因为每次调用渲染函数中的like
值都是一个常量(在各自的渲染函数作用域内)。
渲染输出会变化是因为组件函数被一次次调用,而每一次调用引起的渲染函数中包含的like
值都是函数内部互相独立的。
这就是为什么setTimeout
中拿到的仍然是1
而不是最新的like
。因为闭包的原因,当我们点击getLikeValue
的时候获取的是当次渲染函数内部的like
值,谨记每次渲染state
和prop
都是相互独立的(因为是各自函数作用域内的变量),每次独立渲染函数中的state
和prop
都是保持不变的常量。
每次改变state/props
造成函数组件重新执行,从而每次渲染函数中的state/props
都是独立的,固定的。
注意这里的固定和独立这两个关键字。
- 固定表示函数在开始执行时已经确定了本次
state/props
的值。(特别注意上文setTimeout
因为闭包的原因访问的是固定的,已经确定的state
)。 - 独立表示每次运行函数的
state/props
都是各自独立作用域中的。
useRef
上边我们说到关于state
和props
在不同渲染中的独立性,这个时候就引出了我们的主角useRef
。
useRef
日常主要有两种作用,我们先来说说刚才关于state
碰到的问题,使用useRef
来如何解决。
useRef
作用一:多次渲染之间的纽带
之前通过state
我们了解了,react
中每一次渲染它的state/props
都是相互独立的,于是自然而然我们想到如何在每一次渲染之间产生关系呢。这个时候useRef
就展示了他的作用。
我们先来看看关于useRef
在react
中返回值的类型定义:
interface MutableRefObject<T> {
current: T;
}
可以看到useRef
返回值是一个包括属性current
类型为范型<T>
的一个object
。
它与直接在function compoent
中定义一个{ current:xxx }
的区别就是。
useRef
会在所有的render
中保持对返回值的唯一引用。因为所有对ref
的赋值和取值拿到的都是最终的状态,并不会因为不同的render
中存在不同的隔离。
简单来说,你可以将useRef
的返回值,想象成为一个全局变量。
我们来改写一下这个Demo再来看看:
import React, { useState, useEffect, useRef } from 'react';
export default function Demo() {
const [like, setLike] = useState(0);
const likeRef = useRef(0);
useEffect(() => {});
const handleClick = () => {
setLike(like + 1);
likeRef.current = likeRef.current + 1;
};
const getLikeValue = () => {
setTimeout(() => {
alert(likeRef.current);
}, 2000);
};
return (
<div>
<button onClick={handleClick}>+</button>
<button>{like} 👍</button>
<button onClick={getLikeValue}>获得like值</button>
</div>
);
}
按照之前的步骤操作,这个时候我们可以看到alert
弹出的值就是最新的值。而并非作用域隔离的值。
当然需要额外注意的是,修改
useRef
返回的值并不会引起react
进行重新渲染执行函数,demo中的页面渲染不是因为修改Ref
的值,而是因为我们在修改likeRef.current
时同时修改了state
中的setLike
造成了页面渲染。
useRef
的值改变并不会造成页面重新渲染,这一点可以做很多事情。比如可以配合useEffect
查询页面是首次渲染还是更新。
总结来说,useRef
返回值的改变并不会造成页面更新。而且useRef
类似于react
中的全局变量并不存在不同次render
中的state/props
的作用域隔离机制。这就是useRef
和useState
这两个hook
主要的区别。
useRef
作用二:获取DOM元素
vue3
中获取DOM
当然这一点也是比较常用的useRef
的用法,对比在vue3
中获取DOM节点:
<template>
<div ref="helloRef">Hello</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const helloRef = ref(null)
return {
helloRef
}
}
}
</script>
接下来只需要通过helloRef.value
便可以获得div
节点。
react
中获取DOM
说到上边你可以已经了解了,没错useRef
还有一种用法就是通过它来获取页面上的DOM
元素。
import React, { useState, useEffect, useRef } from 'react';
export default function Demo() {
const domRef = useRef<HTMLInputElement>(null);
useEffect(() => {
domRef.current?.focus();
console.log(domRef,'domRef')
});
return (
<div>
<input ref={domRef} type="text" />
<button>增加</button>
</div>
);
}
- 通过
useRef
创建一个变量进行保存(domRef
)。 - 在
jsx
中通过ref={domRef}
给对应元素节点添加属性。 - 在页面挂载后通过
domRef.current
就可以获取对应节点的真实DOM元素了。
总结
对于上边的Demo,我们可以总结出useRef
的一些特性。
我们可以将useRef
返回值看作一个组件内部全局共享变量,它会在渲染内部共享一个相同的值。相对state/props
他们是独立于不同次render
中的内部作用域值。
同时额外需要注意useRef
返回值的改变并不会引起组件重新render
,这也是和state/props
不同的地方。
当然我们在React.functionComponent
中想要获取对应jsx
的真实Dom
元素时候也可以通过useRef
进行获取到对应的Dom
元素。