1.ref类型:
- React.createRef(): 适用于类组件 版本16.3+
- React.useRef(): 适用于函数组件 版本16.8+
- 回调Ref: 都能用,但非版本不支持不建议用 版本 16.2-
- 字符串ref: 强烈不建议使用,本文也不做相关介绍
2.不要在hooks中使用React.createRef()
import React from 'react'
function Comp() {
const inputRef = React.createRef()
const [num, setNum] = useState(0)
const focus = () => inputRef.current.focus()
const update = () => setNum(num + 1)
return (
<>
<input ref={inputRef} />
<button onClick={focus}>聚焦</button>
<button onClick={update}>点击触发更新 {num}</button>
</>
)
}
我们先来看段代码,如果你跑一下上面这段代码,会发现功能很正。这时候你可能会觉得没有影响,那我们不妨将上面的代码稍微改造一下。
import React from 'react'
function Comp() {
const inputRef = createRef()
const [num, setNum] = useState(0)
useEffect(() => {
setNum(1) // 触发一个更新
setTimeout(() => {
inputRef.current.focus() // 此时input能否正常聚焦?
}, 3000)
}, [])
return <input ref={inputRef} />
}
如果改成这样就会发现,刚刚还正常的现在居然没办法正常聚焦了,为什么会出现这种情况呢,下面来看看具体原因:
这是一张react16.4+的生命周期图谱,从以上我们可以获取到如下两个信息:
1.Ref将会在在组件挂载时的render阶段绑定。
2.Ref将在组件卸载时的commit阶段将解除。
hooks组件的每次更新都是通过重新调用该函数来实现的,此时里面的变量都会重新被创建。我们假设触发更新前的inputRef为inputRef1,触发更新后的inputRef为inputRef2,那么在定时器中的inputRef是inputRef1还是inputRef2呢,这里涉及到闭包的概念,对闭包不熟悉的朋友可以先点击此处了解一下闭包。如果对闭包熟悉的话应该不难知道,此时的inputRef指的是inputRef1,那inputRef1对象这时候的值在上个周期中已经被卸载了,也就是此时inputRef1的值应该是{current: null},所以这个时候调用inputRef.current.focus()会直接报错。
2.React.forwardRef() 适用版本:16.3+
我们在使用React.createRef()与React.useRef()的时候还会存在以下两个问题:
1.无法在函数组件上使用ref属性。
2.在类组件上使用ref属性,只会得到组件实例。
如果父组件的 Ref 对象要传递给子组件的某个 DOM 节点或者更下层,唯一方法只有变通地使用特殊的属性名来传递 Ref 对象。自 React 16.3+ 起,可以使用 React.forwardRef() 方案。使用方法如下:
import React, { useCallback, useRef } from 'react';
import ReactDOM from 'react-dom';
// 实现 ref 的转发
const FancyButton = React.forwardRef((props, ref) => (
<div>
<input ref={ref} type="text" />
<button>{props.children}</button>
</div>
));
// 父组件中使用子组件的 ref
function App() {
const ref = useRef();
const handleClick = useCallback(() => ref.current.focus(), [ ref ]);
return (
<div>
<FancyButton ref={ref}>Click Me</FancyButton>
<button onClick={handleClick}>获取焦点</button>
</div>
)
}
ReactDOM.render(<App />, root);
不过光看上面这个代码,似乎并不符合平时开发中的使用习惯,也会导致代码可读性变差,于是在16.8+的版本中,官方引入了React.useImperativeHandle()。下面我们将尝试用React.useImperativeHandle api对上面这段代码进行简单的改造。
3.React.useImperativeHandle() 适用版本: 16.8+
import React, { useRef, useImperativeHandle } from 'react';
import ReactDOM from 'react-dom';
const FancyInput = React.forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} type="text" />
});
const App = props => {
const fancyInputRef = useRef();
return (
<div>
<FancyInput ref={fancyInputRef} />
<button
onClick={() => fancyInputRef.current.focus()}
>父组件调用子组件的 focus</button>
</div>
)
}
ReactDOM.render(<App />, root);
React.useImperativeHandle实际上就是提供了一个让开发者可以自定义current值的方法,虽然我们仍然需要配合forwardRef使用,但在代码的可读性上来说有了比较明显的提升,也更符合我们平时编程习惯了。
4.回调Ref
回调Ref的使用没有限制,既可以在class component中使用也可以在function component中使用,使用方式如下
import React from 'react';
export default class MyInput extends React.Component {
constructor(props) {
super(props);
this.inputRef = null;
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 一定是最新的。
同时回调ref也是支持在组件间传递的
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}/>
)
}
参考链接: