这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战」
0.Refs 使用场景:
在某些情况下,我们需要在典型数据流之外强制修改子组件,被修改的子组件可能是一个 React 组件的实例,也可能是一个 DOM 元素,例如:
-
管理焦点,文本选择或媒体播放。
-
触发强制动画。
-
集成第三方 DOM 库。
1.createRef:
支持 在函数组件 和 类组件 内部使用
createRef 是 React16.3 版本中引入的。
1.1创建 Refs:
使用 React.createRef() 创建 Refs,并通过 ref 属性附加至 React 元素上。通常在构造函数中,将 Refs 分配给实例属性,以便在整个组件中引用。
1.2访问 Refs:
当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中访问。
import React from 'react';
export default class MyInput extends React.Component {
constructor(props) {
super(props);
//分配给实例属性
this.inputRef = React.createRef(null);
}
componentDidMount() {
//通过 this.inputRef.current 获取对该节点的引用
this.inputRef && this.inputRef.current.focus();
}
render() {
//把 <input> ref 关联到构造函数中创建的 `inputRef` 上
return (
<input type="text" ref={this.inputRef}/>
)
}
}
🌵**ref 的值根据节点的类型而有所不同:**
-
当
ref属性用于HTML元素时,构造函数中使用React.createRef()创建的ref接收底层DOM元素作为其current属性。 -
当
ref属性用于自定义的 class 组件时,ref对象接收组件的挂载实例作为其current属性。 -
不能在函数组件上使用
ref属性,因为函数组件没有实例。
🔮总结:ref挂dom上,就获取dom节点的引用;挂组件上就获取该组件的实例。
-
为 DOM 添加
ref,那么我们就可以通过ref获取到对该DOM节点的引用。 -
而给React组件添加
ref,那么我们可以通过ref获取到该组件的实例【不能在函数组件上使用 ref 属性,因为函数组件没有实例】。
2.useRef:
🌟仅限于在函数组件内使用
useRef 是 React16.8 中引入的,只能在函数组件中使用。
2.1创建 Refs:
使用 React.useRef() 创建 Refs,并通过 ref 属性附加至 React 元素上。
const refContainer = useRef(initialValue);
🌵useRef 返回的 ref 对象在组件的整个生命周期内保持不变。
2.2访问 Refs:
当 ref 被传递给 React 元素时,对该节点的引用可以在 ref 的 current 属性中访问。
import React from 'react';
export default function MyInput(props) {
const inputRef = React.useRef(null);
React.useEffect(() => {
inputRef.current.focus();
});
return (
<input type="text" ref={inputRef} />
)
}
描述:
- 上述代码实现了,当
MyInput组件加载完毕的时候,聚焦焦点事件。
灰常重要的知识点👇
🍊关于 React.useRef() 返回的 ref 对象在组件的整个生命周期内保持不变,我们来和 React.createRef() 来做一个对比,下面来通过代码来立即下:
import React, { useRef, useEffect, createRef, useState } from 'react';
function Myinput(){
let[count,setCount]=useState(0);
const cRef=createRef(null);
const uRef=useRef(null);
//通过[]让useEffect只执行一次
useEffect(()=>{
uRef.current.focus();
window.cRef=cRef;
window.uRef=uRef;
},[]);
useEffect(() => {
//除了第一次为true, 其它每次都是 false 【createRef】
console.log('cRef === window.cRef', cRef === window.cRef);
console.log(cRef);
//始终为true 【useRef】
console.log('uRef === window.uRef', uRef === window.uRef);
console.log(uRef);
})
return (
<>
<input type="text" ref={uRef}/>
<input type="text" ref={cRef}/>
<button onClick={()=>{setCount(count+1)}}>{count}</button>
</>
)
}
export default Myinput;
解释:
useRef不管组件怎么更新始终是一个引用- 而
createRef不是,每次生成换一个引用 useRef返回的ref 对象在组件的整个生命周期内保持不变,也就是说每次重新渲染函数组件时,返回的ref 对象都是同一个(使用React.createRef,每次重新渲染组件都会重新创建 ref)
2.3回调 Refs:
支持在函数组件和类组件内部使用
React 支持 回调 refs 的方式设置 Refs。这种方式可以帮助我们更精细的控制何时 Refs 被设置和解除。
**使用 回调 refs 需要将回调函数传递给 React元素 的 ref 属性。**这个函数接受 React 组件实例 或 HTML DOM 元素作为参数,将其挂载到实例属性上,如下所示:
import React from 'react';
export default class MyInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = null;
//ref回调
this.setTextInputRef = (ele) => {
this.inputRef = ele;
}
}
componentDidMount() {
this.inputRef && this.inputRef.focus();
}
render() {
return (
<input type="text" ref={this.setTextInputRef}/>
)
}
}
🌵挂载和卸载的两种情况:
-
组件挂载:React 会在组件挂载时,调用
ref回调函数并传入 DOM元素(或React实例); -
组件卸载:当卸载时调用它并传入
null。在componentDidMount或componentDidUpdate触发前,React 会保证 Refs 一定是最新的。
可以在组件间传递回调形式的 refs.👇(组件间是可以把ref当做props来传递)
import React from 'react';
//父组件
export default function Form() {
let ref = null;
React.useEffect(() => {
//ref 即是 MyInput 中的 input 节点
ref.focus();
}, [ref]);
return (
<>
<MyInput inputRef={ele => ref = ele} />
{/** other code */}
</>
)
}
//子组件:
function MyInput (props) {
return (
<input type="text" ref={props.inputRef}/>
)
}
3.Ref的传递:
在 Hooks 之前,高阶组件(HOC) 和 render props 是 React 中复用组件逻辑的主要手段。
🌵尽管高阶组件的约定是将所有的 props 传递给被包装组件,但是 refs 是不会被传递的,事实上, ref 并不是一个 prop,和 key 一样,它由 React 专门处理。
这个问题可以通过 React.forwardRef (React 16.3中新增)来解决。
在 React.forwardRef 之前,这个问题,我们可以通过给容器组件添加 forwardedRef (prop的名字自行确定,不过不能是 ref 或者是 key).
🚀 React.forwardRef之前:
import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';
const withData = (WrappedComponent) => {
class ProxyComponent extends React.Component {
componentDidMount() {
//code
}
//这里有个注意点就是使用时,我们需要知道这个组件是被包装之后的组件
//将ref值传递给 forwardedRef 的 prop
render() {
const {forwardedRef, ...remainingProps} = this.props;
return (
<WrappedComponent ref={forwardedRef} {...remainingProps}/>
)
}
}
//指定 displayName. 未复制静态方法(重点不是为了讲 HOC)
ProxyComponent.displayName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
//复制非 React 静态方法
hoistNonReactStatic(ProxyComponent, WrappedComponent);
return ProxyComponent;
}
这个示例中,我们将 ref 的属性值通过 forwardedRef 的 prop,传递给被包装的组件,使用:
class MyInput extends React.Component {
render() {
return (
<input type="text" {...this.props} />
)
}
}
MyInput = withData(MyInput);
function Form(props) {
const inputRef = React.useRef(null);
React.useEffect(() => {
console.log(inputRef.current)
})
//我们在使用 MyInput 时,需要区分其是否是包装过的组件,以确定是指定 ref 还是 forwardedRef
return (
<MyInput forwardedRef={inputRef} />
)
}
有了React.forwardRef之后:
Ref 转发是一项将ref自动地通过组件传递到其一子组件的技巧,其允许某些组件接收 ref,并将其向下传递给子组件。
转发 ref 到DOM中:
import React from 'react';
//子组件
const MyInput = React.forwardRef((props, ref) => {
return (
<input type="text" ref={ref} {...props} />
)
});
//父组件
function Form() {
//创建ref
const inputRef = React.useRef(null);
React.useEffect(() => {
console.log(inputRef.current);//input节点
})
return (
<MyInput ref={inputRef} />
)
}
解释:
-
调用
React.useRef创建了一个React ref并将其赋值给inputRef变量。 -
指定
ref为JSX属性,并向下传递 MyInput ref={inputRef} -
React 传递
ref给forwardRef内函数(props, ref) => ...作为其第二个参数。 -
向下转发该
ref参数到 button ref={ref},将其指定为JSX属性 -
当
ref挂载完成,inputRef.current指向inputDOM节点
🍀注意:
第二个参数 ref 只在使用 React.forwardRef 定义组件时存在。常规函数和 class 组件不接收 ref 参数,且 props 中也不存在 ref。
4.useImperativeHandle的使用:
useImperativeHandle(ref, createHandle, [deps])
useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值(说白了就是你想让父组件看到什么,可以进行通过这个useImperativeHandle进行设置)。
在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef 一起使用:
function FancyInput(props, ref) {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
用途:
-
useImperativeHandle可以让你在使用ref时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛 -
在大多数情况下,应当避免使用
ref这样的命令式代码。useImperativeHandle应当与forwardRef一起使用 -
父组件可以使用操作子组件中的多个
ref
例子👇:
import React,{useState,useEffect,createRef,useRef,forwardRef,useImperativeHandle} from 'react';
//子组件:
function Child(props,parentRef){
// 子组件内部自己创建 ref
let focusRef = useRef();
let inputRef = useRef();
useImperativeHandle(parentRef,()=>(
// 这个函数会返回一个对象
// 该对象会作为父组件 current 属性的值
// 通过这种方式,父组件可以使用操作子组件中的多个 ref
return {
focusRef,
inputRef,
name:'计数器',
focus(){
focusRef.current.focus();
},
changeText(text){
inputRef.current.value = text;
}
}
});
return (
<>
<input ref={focusRef}/>
<input ref={inputRef}/>
</>
)
}
ForwardChild = forwardRef(Child);
//父组件:
function Parent(){
const parentRef = useRef();//{current:''}
function getFocus(){
parentRef.current.focus();
// 因为子组件中没有定义这个属性,实现了保护,所以这里的代码无效
parentRef.current.addNumber(666);
parentRef.current.changeText('<script>alert(1)</script>');
console.log(parentRef.current.name);
}
return (
<>
<ForwardChild ref={parentRef}/>
<button onClick={getFocus}>获得焦点</button>
</>
)
}
目前笔者总结这里以上几种场景吧,后续遇到其他新的使用场景会陆续再更新进来...