一网打尽 React 的 Refs

大家好呀,我是晨光,近来一两周再升级 RN,遇到了不少问题,其中有一个问题和 Ref 有关,查了老半天,才发现是高版本不支持了,需要改改写法。


一、什么是 Refs


Refs 提供了一种访问在 render 方法中创建的 DOM 节点或 React 元素的方法。


在常规的 React 数据流中,Props 是父子组件交互的唯一方式。如果要修改子元素,需要使用 State 或者 Props 触发它重新渲染。但某些情况下,有些组件可能需要控制和同步 React 之外的系统,例如输入框聚焦,操作视频播放等。或是操作 DOM 滚动到某个位置,亦或是测量元素的尺寸和位置等等。


对于这些功能,React 官网的英文标题是 Escape Hatches,译为 脱围机制,描述很贴切,脱离 React 常规用法之外的一些功能。


二、什么时候用 Ref


1、“记住”某些信息


当你希望组件“记住”某些信息,但又不想让这些信息触发新的渲染时,就可以使用 ref。函数式组件中,常用 useRef 这个 hook 函数来创建一个 ref

const ref = useRef(0);

与 state 一样,ref 在重新渲染之间由 React 保留。但是,设置 state 会重新渲染组件,而更改 ref 不会!可以通过 ref.current 属性访问该 ref 的当前值。

import { useRef, useState } from "react";

export default function Counter() {
let ref = useRef(0);
const [num, setNum] = useState(0);
let testNum = 0;

console.log("testNum", testNum);
console.log("num", num);
console.log("ref.current", ref.current);

function handleClick() {
setNum(num + 1);
testNum = testNum + 1;
ref.current = ref.current + 1;
console.log("handleClick testNum", testNum);
console.log("handleClick num", num);
console.log("handleClick ref.current", ref.current);
alert("你点击了 " + ref.current + " 次!");
}

return <button onClick={handleClick}>点我!{num}</button>;
}

上面代码,实现了一个按钮,点击改变 ref.current 的值和 State num 的值,而且随着 num 的变化,组件的重新渲染,ref.current 一直保留下来,而定义的常量 testNum,随着每次组件的刷新,testNum 都只是默认值。

1)组件第一次渲染时,初始状态都为 0

2)点击按钮后,testNum 和 ref.current 可以理解为是常量,所以都立即更新了,但 num 是 state,需要走一次异步的 setState,所以并没有立马变成 1

3)在 state 变更结束后,组件重新渲染,state 刷新,ref.current 值保留,但定义的常量 testNum 又被重置到了 0


基于上面的这个 demo,可以更清晰的理解,如果要在组件内‘记住’某个值,可以使用 ref.current


ref 可以是任何值,字符串,数字,或者是对象,甚至函数,它是一个普通的 JavaScript 对象具有可以被读取和修改的 current 属性


2、DOM 操作


一般情况下都不会用到 DOM 操作,因为 React 已经帮我们封装好了,但是在某些业务场景下,点击了某个位置需要输入框聚焦,又或是点击按钮需要页面滚动到指定位置等,这时候 Ref 的 DOM 操作就能派上用场了。


import { useRef } from 'react';

export const Demo = () => {
const myRef = useRef(null);
return <div ref={myRef}> demo </div>
}


当 React 为这个 div 创建一个 DOM 节点时,就会把对该节点的引用放入 myRef.current。然后我们就可以访问此 DOM 节点,并使用在其上定义的内置一些浏览器 API。


1)设置 Input 聚焦

import { useRef } from 'react';

export default function Form() {
const inputRef = useRef(null);

function handleClick() {
inputRef.current.focus();
}

return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}

步骤分解:

1️⃣ 创建一个 inputRef 并且要传到 input 的 ref 属性上

2️⃣ 按钮上绑定 inputRef.current 操作了 DOM 的方法


如此,就能实现点击按钮,input 聚焦的功能。


2)滚动到某个位置/某个元素


假设,一个页面,里面元素比较多,点击页面上某个按钮的时候,需要滚动到页面上某个元素的位置。


步骤分解:

1️⃣ 页面容器根元素需要绑定一个 rootRef

2️⃣ 页面上 需要滚到特定元素位置的元素需要绑定一个 itemRef,如果这样的元素有多个,可以使用一个 Map 对象把它们的 ref 存起来,使用时可根据存入的 ID 匹配

3️⃣ 页面上的操作按钮,需要绑定 rootRef 操作 dom 的方法,传参是 Map 对象的 ID


import { useRef } from 'react';

export default function CatFriends() {
// 1、使用 ref 存储 整个 list 的 node 节点
const itemsRef = useRef(null);

function scrollToId(itemId) {
const map = getMap();
// 4、根据 id 匹配到 ref
const node = map.get(itemId);
// 5、调用 node 节点的一些 DOM 操作的方法
node.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center'
});
}

function getMap() {
if (!itemsRef.current) {
// 2、首次运行时初始化 Map。
itemsRef.current = new Map();
}
return itemsRef.current;
}

return (
<>
<button onClick={() => scrollToId(9)}>
滚动到 9 的位置
</button>
<div>
<ul>
{catList.map(cat => (
<li
key={cat.id}
ref={(node) => {
const map = getMap();
if (node) {
// 3、通过 ref 把 node 节点存到 map 中
map.set(cat.id, node);
}
}}
>
<div>{cat.id}</div>
</li>
))}
</ul>
</div>
</>
);
}

const catList = [];
for (let i = 0; i < 40; i++) {
catList.push({ id: i });
}


上述代码也分解了代码的步骤 12345,元素锚定也可以采用这样的方式。


3)访问别的组件的 DOM 节点


需要用到 forwordRef 方法。第一个参数是 props,第二个是 ref,代码如下:

import { forwardRef, useRef } from 'react';

const MyInput = forwardRef((props, ref) => {
return <input {...props} ref={ref} />;
});

export default function Form() {
const inputRef = useRef(null);

function handleClick() {
inputRef.current.focus();
}

return (
<>
<MyInput ref={inputRef} />
<button onClick={handleClick}>
聚焦输入框
</button>
</>
);
}


当然我们还可以使用useImperativeHandle 来限制 ref 对外暴露的方法,可以利用该方法封装一层再对外暴露。修改一下 MyInput 组件

import {
forwardRef,
useRef,
useImperativeHandle
} from 'react';

const MyInput = forwardRef((props, ref) => {
const realInputRef = useRef(null);
useImperativeHandle(ref, () => ({
// 只暴露 focus,没有别的
focus() {
realInputRef.current.focus();
},
}));
return <input {...props} ref={realInputRef} />;
});

再使用新的 MyInput 组件时,调用 handleClick 方法内的 focus 方法,是封装后的返回的 focus 方法。而不是 DOM 节点的 focus 方法。


三、ReactNative 中的 Ref


最近在在做的 RN 升级,是从 0.71 => 0.72.5,看似一个小版本升级,实则改动很多,而且底层的 Native 侧也需要适配很多东西,还有Android 和 IOS 的表现还会不太一样,导致升级的过程很痛苦。


遇到和 Ref 相关的问题是,Animated.ScrollView 组件在实现锚定功能时,低版本 RN 需要用到 findNodeHandle 方法去获取 ref 控件的 tag 值,将这个值传入到 IOS 原生中,再用利利用这个 tag 获取当当前的 view,根据 view 获取当前 VC,用 VC 去跳转页面且传值使用,最后完成调用。


RN 侧的代码如下,安卓跑起来没问题,IOS 挂了

// 部分代码
const scrollViewHandler = findNodeHandle(this.scrollView);
if (scrollViewHandler === null) {
return;
}

anchorView.measureLayout(
scrollViewHandler,
(_, top) => {
this.scrollView?.scrollTo({
y: top - this.headerHeight,
animated: needAnimated,
});
},
() => {}
);

挂的原因, 0.72.5 的 RN 不再支持 findNodeHandle 方法,在measureLayout 方法直接传入 ref 即可,也就是改为下面这样

anchorView.measureLayout(
this.scrollView, // 直接传入 Ref
(_, top) => {
this.scrollView?.scrollTo({
y: top - this.headerHeight,
animated: needAnimated,
});
},
() => {}
);

过程有点子曲折,这里就不展开说了,一点小经验,遇到问题,比如 API 更新用法啦,哪些不支持啦,上官网查,效率贼啦高!!


四、总结


1、ref 可以存储一些需要记忆的值,但可以不用关心试图的重新渲染

2、ref 可以通过 useRef 创建,ref.current 是我们要使用的字段

3、默认情况下,自定义组件不暴露其 DOM 节点。 可以通过使用 forwardRef 并将第二个 ref 参数传递给特定节点来暴露 DOM 节点。即可以通过 forwardRef 函数传递 ref

4、可以通过 useImperativeHandle 函数限制 ref 对外暴露的方法

5、需要注意,尽量避免 ref 更改由 React 管理的 DOM 节点,否则容易造成页面 DOM 节点混乱从而引发报错。


React 官网有一些 Demo 还有一些小挑战,感兴趣的同学可以去看看

https://react.dev/learn/referencing-values-with-refs


以上,全文完,有收获记得点个赞呀👍~

#前端

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
下载 APP