【前端小课堂】—— React中Refs
一、关于Refs的概念
Refs是什么?用来做什么?
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 组件实例。
在React中,提倡数据驱动视图的更新模式。
父组件与子组件最理想的通信方式是,通过子组件的props把数据传入子组件内部;子组件通过事件把组件内部数据,传出给父组件。从而实现数据的内外同步,进而实现组件状态的改变,最后反映到视图的更新。
在这种模式下,视图会跟随数据逻辑的变化,而自动更新。开发者可以把更多的精力放在数据逻辑上面。
在极少数情况下,我们可能需要强制修改元素或者修改元素的属性。比如,input获取焦点;DOM元素附加样式等。
为了解决此类问题,React引入了Refs相关api,用于访问目标元素。目标元素可以是DOM元素或者组件实例。
为了书写方便,下面我们一致使用目标元素来代替“DOM元素或者组件实例”。
二、创建Refs的几种方式
1、使用createRef创建Refs
通常使用 createRef() 创建ref对象,进而把ref对象赋值给目标元素的ref属性,然后通过ref.current可以访问到目标元素。
class Test extends React.Component {
constructor(props) {
super(props);
this.testRef = React.createRef()
}
log(){
console.log(this.testrRef.current.value)
}
render() {
return <input ref={this.testRef} />
}
}
2、使用回调形式的Refs
上面的例子使用在 React 16.3 版本引入的 createRef() API创建ref对象。较早版本的 React,可以使用回调形式的 Refs,创建ref对象。
React在组件挂载时,会调用 ref 回调函数并传入目标元素,当卸载时再次调用并传入null。而且在 componentDidMount 或 componentDidUpdate 触发前都会调用回调函数,以保证 ref对象绑定的目标对象是最新的。
我们推荐使用这种方式创建 ref对象。
需要注意,回调函数直接引用了目标元素,不需要current属性访问。
class Test extends React.Component {
constructor(props) {
super(props);
this.setRef = (target) => {
this.testRef = target;
};
}
log() {
console.log(this.testRef?.value);
}
render() {
return (
<div>
<input defaultValue="abc" ref={this.setRef} />
<button onClick={() => this.log()}>显示ref</button>
</div>
);
}
}
3、使用字符串声明Refs
如果ref属性值是字符串,React会自动创建函数,处理ref逻辑。这种方式创建的ref对象,会自动保存到this.refs上面。我们可以通过this.refs.refName访问到ref对象。
官方已经不建议使用这个方式,未来可能会被移除。
class TestComponent extends React.Component {
constructor(props) {
super(props);
}
log() {
console.log(this.refs.testRef.value);
}
render() {
return (
<div>
<input defaultValue="abc" ref='testRef' />
<button onClick={() => this.log()}>显示ref</button>
</div>
);
}
}
4、使用useRef
class组件中,使用createRef()语法,创建ref对象。但createRef()语法并不适用于函数组件。 因为createRef方法的功能,只是创建并返回一个带current属性的对象。如果函数组件中使用createRef(),每次渲染都会重新创建一个新的对象,无法保留最初创建的ref对象。
参考createRef方法的源码:
export function createRef() {
const refObject = {
current: null,
}
return refObject
}
为了解决这个问题,React官方引入了useRef hook,用于函数组件中创建ref对象。使用useRef()创建的ref对象,和函数组件对应的 fiber 对象关联。因此,ref对象在组件整个生命周期一直存在,直到组件实例被销毁。
const Test = ()=> {
const ref = React.useRef();
return (
<div
onClick={() => {
console.log(ref.current);
}}
>
<input ref={ref} />
</div>
);
}
三、使用Refs的场景
使用Refs的场景有很多。最常见的场景是,访问目标元素。目标元素可以是DOM元素,或者组件实例。
除此之外,在某些情况下,我们可能需要在父组件中,访问子组件内部的目标元素。但ref属性已经用于访问子组件,没有办法传入子组件内部,访问目标元素。这种情况需要用的forwardRef api。
另外,Refs做为组件整个生命周一直存在的普通对象,设置ref.current不会触发组件的重新渲染。我们可以借用ref对象存储一些变量,用于实时访问。
1、引用 DOM 元素
把创建的ref对象,赋值给DOM元素的ref属性,可以实现对DOM元素的引用。
下面例子中,我们把testRef赋值给了input的ref属性。这样我们就可以通过testRef.current访问到input元素。然后执行input的focus()方法,使input获取焦点。
class Test extends React.Component {
constructor(props) {
super(props);
this.testRef = React.createRef();
}
action() {
this.testRef.current.focus();
}
render() {
return (
<div>
<input defaultValue="abc" ref={this.testRef} />
<button onClick={() => this.action()}>引用input</button>
</div>
);
}
}
2、引用组件实例
除了可以引用指定的dom元素,ref还可以引用组件实例。
下面例子中,我们把testRef赋值给Input组件的ref属性。同样,我们可以通过testRef.current,访问到Input组件的实例。
class Input extends React.Component {
constructor(props) {
super(props)
this.state = { name: 'abc' }
}
render() {
return (
<input
value={this.state?.name}
onChange={(e) => this.setState({ name: e.target.value })}
/>
)
}
}
const Test = () => {
const testRef = React.useRef(null)
const log = () => {
console.log(testRef?.current?.state.name)
}
return (
<div>
<Input ref={testRef} />
<button onClick={log}>访问Input</button>
</div>
)
}
有一点需要注意,因为函数组件没有实例,所以ref不能引用函数组件。
3、Refs 转发
除了访问DOM元素 和 组件实例,有时候我们还需要访问组件内部的元素。比如,访问Button组件内部的button元素,访问Input组件内部的input元素。
令人遗憾的是,我们无法直接通过ref属性把ref对象传入组件内部。因为,ref属性会被特殊处理掉,用于访问组件实例本身。
当然,我们可以换一个props属性,把ref对象传入组件内部,然后访问目标元素。
比如:
const Text = ({ children, testRef }) => <div ref={testRef}>{children}</div>;
const Test = () => {
const ref = React.useRef();
useEffect(() => {
console.log(ref.current);
}, []);
return (
<div>
<Text testRef={ref}>hello</Text>
</div>
);
}
a、使用forwardRef转发
实际上React官方为了解决此类问题,引入了forwardRef。forwardRef接受一个render函数做为参数,并返回一个组件。这个组件可以接受ref属性,并通过函数组件的第二个参数,转发到组件内部。
下面例子中,Text组件,经过forwordRef处理,所以ref属性不会像普通组件那样被特殊处理掉。而是作为render函数的第二个参数,传入到了组件内部,用于访问组件内部的div。
const Text = forwardRef({children}, ref) => <div ref={ref}>{children}</div>;
const Test = () => {
const ref = React.useRef();
useEffect(() => {
console.log(ref.current);
}, []);
return (
<Text ref={ref}>hello</Text>
);
}
需要注意,普通函数组件,和class组件的构造函数,没有ref参数。
b、在高阶组件中转发
高阶组件(HOC)会透传(pass through)所有props到其包裹的组件,但是不会透传ref,ref同样会被特殊处理掉。因此下面例子中,ref对象绑定的是Log组件,而非TestButton。
function log(Cmp) {
class Log extends React.Component {
componentDidUpdate(prevProps) {
console.log(prevProps);
}
render() {
return <Cmp {...this.props} />;
}
}
return Log;
}
class TestButton extends React.Component {
focus() {
// ...
}
}
export default log(TestButton)
import TestButton from './TestButton'
const Test = () => {
const ref = React.useRef();
useEffect(() => {
console.log(ref.current);
}, []);
return (
<TestButton ref={ref}></TestButton>
);
}
为了解决上面这个问题,同样需要借用forwardRef把ref转发到HOC内部被包裹的组件。
下面我们调整一下,高阶组件log的实现。实际上,这里用到了前面刚才讲过的知识。
- 声明一个render函数,借用forwardRef()生成新的组件,用于接受和传递ref对象;
- 换一个props名称(forwardedRef)把接收到的ref对象传入高阶组件内部;
- 把ref对象,赋值给内部组件的ref属性,从而访问到内部组件
function log(Cmp) {
class Log extends React.Component {
componentDidUpdate(prevProps) {
console.log(prevProps);
}
render() {
let { forwardedRef, ...rest } = this.props;
return <Cmp {...rest} ref={forwardedRef} />;
}
}
const forward = (props, ref) => {
return <Log {...props} forwardedRef={ref} />;
};
return React.forwardRef(forward);
}
c、使用useImperativeHandle定制暴露属性
首先,我们要时刻谨记,ref是破坏性的,应当尽量避免使用。即使极少数情况,不得不使用ref,也应当尽量减少组件对外暴露属性。
而且还有一个问题需要解决。ref不能直接访问函数组件,更无法使用函数组件内部定义的方法和state。
于是,React引入useImperativeHandle。useImperativeHandle是hooks,只能在函数组件中使用,而且需要结合forwordRef一起使用。
const Input = (props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
getVal: () => {
return inputRef.current.value;
},
}));
return <input defaultValue="test" ref={inputRef} />;
};
export default forwardRef(Input);
import Input from './input'
const Test = () {
const ref = useRef();
useEffect(() => {
console.log(ref.current);
}, []);
return (
<Input ref={ref} />
);
}
只暴露了,getVal()
当然,你也可以通过相同的思路,暴露函数组件内部的属性或者方法。
const Test = (props, ref) => {
const [vs, setVs] = useState(false);
useImperativeHandle(ref, () => ({
getVisible: () => {
return vs;
},
setVisible: (vs) =>{
setVs(vs)
}
}));
return <SomeElement visible={vs} />;
}
export default forwardRef(Test)
4、函数组件中存储数据
函数组件中,useRef()创建的ref对象,能够在函数组件保持用一个引用对象。而且修改ref对象的current,不会触发组件的重新渲染。基于以上特性,可以在ref对象中存储一些数据。
a、存储稳定数据
有时候,我们需要在函数组件中存储一些稳定的数据。比如,setInterval()的id,以便后面清除setInterval。
const Test = () => {
const timerID = useRef();
const action = useCallback(() => {
timerID.current = setInterval(()=>{
// ...
}, 1000);
},[]);
useEffect(()=>{
return () => clearInterval(timerID.current);
},[]);
return <button onClick={action}>执行</button>
}
b、存储previous value
ref对象,始终保持引用对象;而且dom render早于useEffect执行。
const Previous = () => {
const [count, setCount] = useState(0);
const preRef = useRef();
useEffect(() => {
preRef.current = count;
}, [count]);
return (
<div>
<section>previous: {preRef.current}</section>
<section>current: {count}</section>
<footer>
<button onClick={() => setCount(count + 1)}>add 1</button>
</footer>
</div>
);
}
可以简单封装usePrevious hooks
const usePrevious = (val) => {
const [cur, setCur] = useState(val);
const preRef = useRef();
useEffect(() => {
preRef.current = cur;
}, [cur]);
return [preRef.current, cur, setCur];
};
const Previous = () => {
const [pre, cur, setCur] = usePrevious(0);
return (
<div>
<section>previous: {pre}</section>
<section>current: {cur}</section>
<footer>
<button onClick={() => setCur(cur + 1) }>add 1</button>
</footer>
</div>
);
};
原则上,react并不提倡使用Refs。Refs被定义为逃生舱api。所谓逃生舱,只有在没有办法的情况下才会采用的方法。
转载自:https://juejin.cn/post/7213328766475059261