React与Vue中的Ref

React中的ref:

在React中,ref是一种特殊属性,它可以附加到任何组件上。ref的主要作用是允许我们直接访问DOM元素或者组件实例。这通常是为了管理焦点、触发强制动画或集成非React的库所必需的。由于React的数据流通常是单向且自顶向下的,因此ref提供了一种逃避这种模式以直接交互的方法。最常见的ref使用场景包括:

  • 管理输入焦点。
  • 触发强制动画。
  • 集成第三方DOM库。

React的ref并不推荐频繁使用,因为它违背了React的声明式编程原则。不过,在无法避免直接操作DOM或组件实例时,ref是一个非常有用的逃生窗。

Vue中的ref:

在Vue中,ref也是一个特殊的属性。当它用于HTML元素上时,可以用来获取真实的DOM元素;当它用于组件上时,可以用来获取组件实例。这与React中的ref相似,Vue的ref属性同样用于直接访问DOM节点或子组件的实例。然而,Vue中的ref与数据流动无关,它不依赖于Vue的响应式系统更新。在Vue中,通常可以这样使用ref:

1
2
3
4
5
6
7
8
9
10
11
<template>
<input ref="myInput">
</template>

<script>
export default {
mounted() {
this.$refs.myInput.focus();
}
}
</script>

在上面的例子中,Vue实例的$refs对象将包含一个名为myInput的属性,指向对应的DOM元素。

总的来说,React和Vue中的ref都提供了一种访问DOM元素和子组件实例的方式,但在React中经常需要更多的引导来创建和管理ref,如使用React.createRef()useRef();而在Vue中,ref则与Vue实例的$refs对象直接关联,使用起来可能更直观简单一些。两者都让开发者可以绕过框架的抽象层直接与DOM元素或组件实例交互,但应该谨慎使用以避免破坏框架的响应式和组件化原则。

React类组件中ref的使用方式

stringRef-ref属性值为字符串

在React中,使用字符串形式的ref是一个较旧的API,它允许你将ref赋予一个字符串而不是一个回调函数或createRef创建的对象。然而,这种字符串ref的方式已经被认为是过时的,因为它存在一些问题,比如它们不能和React的新功能,如Hooks,一起使用,并且可能会在未来的React版本中被完全移除。

使用方法类似Vue中ref的使用,以下是示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/*
* @Description:
* @Author: xiuji
* @Date: 2023-12-21 14:46:49
* @LastEditTime: 2023-12-22 09:30:49
* @LastEditors: Do not edit
*/
import React, { Component } from 'react';

class Demo extends Component {
render() {
return (
<div className='test' ref='Demo'>
<div className="string" ref='stringRef'>stringRef</div>
</div>
);
}
// virtualDom已经转化为真实dom,可以获取真实dom
componentDidMount() {
// 受控组件,基于数据驱动视图
// 非受控组件,基于dom操作实现想要的效果
// js获取dom
console.log(document.querySelector('.test'),'js');
// react中通过refs获取dom(不推荐使用)
console.log(this.refs.Demo, 'stringRef');
}
}

export default Demo;

CallbackRef-ref属性值为函数

在React中,ref可以设置为一个回调函数,这是一种更为推荐的使用refs的方式。这个回调函数会在组件挂载(mount)或卸载(unmount)时被调用,并且会接收到底层的DOM元素或类组件的实例作为参数。

这里是一个使用函数形式ref的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
* @Description:
* @Author: xiuji
* @Date: 2023-12-21 14:46:49
* @LastEditTime: 2023-12-22 09:34:50
* @LastEditors: Do not edit
*/
import React, { Component } from 'react';

class Demo extends Component {
render() {
return (
<div className='test' ref='Demo'>
<div className='function'>
<input type="text" ref={x => this.inputDom = x} />
</div>
</div>
);
}
// virtualDom已经转化为真实dom,可以获取真实dom
componentDidMount() {
// 受控组件,基于数据驱动视图
// 非受控组件,基于dom操作实现想要的效果
// 把ref属性值设置为函数,函数的参数就是当前元素的真实dom(或者为类组件实例)
console.log(this.inputDom, 'inputDom');
this.inputDom.focus(); // 页面渲染完毕输入框自动聚焦
}
}

export default Demo;

在这个示例中,当组件挂载时,ref回调函数会被执行,X参数引用了底层的DOM元素。然后,这个DOM元素被赋值给this.inputDom,使得你可以在componentDidMount生命周期方法中,或者在组件的其他任何方法中访问和操作这个DOM元素。

函数形式的ref有几个优点:

  1. 更大的灵活性:函数ref让你有更多的掌控权,可以根据组件的挂载/卸载状态来实施相关操作,比如可以在组件卸载时进行一些清理工作。

  2. 兼容性:函数ref兼容React的所有版本,包括最新的函数组件和使用Hooks的代码。

  3. 简洁性:回调形式的ref可以让你在不使用额外属性或不需要创建React.createRef的情况下,将ref存储在组件实例的一个属性上。

当然,如果你使用的是React 16.3或更高版本,并且你的组件是类组件,推荐使用React.createRef来创建ref。对于函数组件,推荐使用useRef Hook。这些新的API提供了一种更为清晰和更为“React”的方式来处理refs。

createRef

在React中,createRef是创建引用(refs)的一种新方式,它被引入在React 16.3版本,用以替代字符串引用和回调形式的refs。createRef提供了一种更为干净、明确的方式来访问React元素或组件的实例。

使用createRef,你会在组件的构造函数中创建一个ref,然后将其赋值给组件的属性。当组件被渲染时,React会将对应的DOM元素或组件实例赋值给current属性。

这里是一个使用createRef的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react';

class MyComponent extends Component {
constructor(props) {
super(props);
// 创建一个ref来存储textInput的DOM元素
this.textInput = React.createRef();
}

componentDidMount() {
// 当组件挂载后,使用current属性访问DOM节点并调用focus方法
this.textInput.current.focus();
}

render() {
// 告诉React我们想把 <input> ref关联到构造器里创建的`textInput`上
return <input type="text" ref={this.textInput} />;
}
}

在这个组件生命周期中:

  1. 构造函数中使用React.createRef创建了一个ref(this.textInput)。
  2. render方法中,将这个ref(this.textInput)附加到<input>元素上。
  3. 当组件挂载到DOM(componentDidMount)后,使用this.textInput.current来获取DOM元素,并执行操作,例如设置焦点。

使用createRef的好处是它提供了一种更加一致的方式来管理refs。这是目前类组件中推荐的方式来访问Refs。对于函数组件,则可以使用useRef Hook来达到类似的目的。

useRef

useRef 是 React 提供的一个 Hook,它在函数组件中用于获取对 DOM 元素或组件实例的引用。与 createRef 不同,它是专门为函数组件设计的,因为在函数组件中没有实例(this)来附加ref。

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传递给 useRef 的参数(初始值)。返回的对象将在组件的整个生命周期内保持不变。

下面是一个使用 useRef 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React, { useRef, useEffect } from 'react';

function MyComponent() {
// 创建一个ref来存储textInput的DOM元素
const textInput = useRef(null);

useEffect(() => {
// 当组件挂载后,textInput.current指向了 <input> DOM节点
textInput.current.focus();
}, []); // 空依赖数组确保effect只在挂载时运行

// 将textInput ref附加到 <input> 元素上
return <input type="text" ref={textInput} />;
}

在此示例中,useRef 用于创建一个引用 textInput,该引用连接到输入元素上。在组件首次渲染后,useEffect 钩子被调用,并且 .current 属性被设置为对应的 DOM 节点,这允许你直接操作该 DOM 元素。

useRef 的一个关键特性是在组件的不同渲染之间保持不变。因此,你可以用它来存储任何可变值,这个值在组件的整个生命周期中都不会改变。一个常见的用例是跟踪之前的 props 或 state 的值。

1
2
3
4
5
const prevValue = useRef();

useEffect(() => {
prevValue.current = someValue;
}, [someValue]); // 只有当someValue改变时才运行

在这个例子中,prevValue 用于存储 someValue 的前一个值。

记住,对 ref 对象的 .current 属性的修改不会触发组件重新渲染。如果你需要在修改引用时触发重新渲染,可能需要状态钩子 useStateuseReducer

React函数组件中ref的使用方式

函数组件无法直接使用ref属性

在 React 中,不能直接对一个函数组件使用 ref 属性,因为它们没有实例。尝试对一个函数组件添加 ref 将会导致运行时错误。例如,如果尝试下面这种做法,React 将会抛出一个警告:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MyFunctionalComponent() {
return <input />;
}

class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}

render() {
// 这将不会工作,并且React会抛出警告!
return <MyFunctionalComponent ref={this.myRef} />;
}
}

为了让这种模式工作,必须使用 React 提供的一种特殊方法 React.forwardRef。这允许函数组件接收一个 ref,并将其进一步传递给子组件。

例如,如果想让函数组件能够接收一个 ref,可以这样修改它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const MyFunctionalComponent = React.forwardRef((props, ref) => {
return <input ref={ref} />;
});

class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}

render() {
// 现在这将会工作,因为MyFunctionalComponent使用了forwardRef
return <MyFunctionalComponent ref={this.myRef} />;
}
}

在这个例子中,MyFunctionalComponent 被定义为一个使用 React.forwardRef 的函数,这使它可以接受一个 ref 作为第二个参数并将其附加到 DOM 元素上。然后,在父组件中,可以像对待类组件一样对待这个函数组件,并将一个 ref 传递给它。

通过ref控制子组件的行为

在 React 中,通常推荐通过 props 来控制子组件的行为,而不是直接调用子组件的方法。不过,如果确实需要在父组件中获取子组件的实例并调用其方法,可以通过 ref 来实现。对于类组件,这比较直接,因为类组件有实例,但对于函数组件,则需要使用 React.forwardRef 以及使用 useImperativeHandle Hook 来暴露方法。

让我们通过两个例子来说明这一点:

类组件中的子组件:

如果子组件是一个类组件,可以直接给它一个 ref 并调用它的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ChildComponent extends React.Component {
someMethod() {
// 这是子组件的一个方法
}

render() {
return <div>Child Component</div>;
}
}

class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.childRef = React.createRef();
}

callChildMethod = () => {
if (this.childRef.current) {
this.childRef.current.someMethod();
}
};

render() {
return (
<>
<ChildComponent ref={this.childRef} />
<button onClick={this.callChildMethod}>Call Child Method</button>
</>
);
}
}

函数组件中的子组件:

如果子组件是一个函数组件,需要使用 React.forwardRefuseImperativeHandle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { useImperativeHandle, forwardRef } from 'react';

const ChildComponent = forwardRef((props, ref) => {
useImperativeHandle(ref, () => ({
someMethod() {
// 这是子组件的一个方法
}
}));

return <div>Child Component</div>;
});

class ParentComponent extends React.Component {
constructor(props) {
super(props);
this.childRef = React.createRef();
}

callChildMethod = () => {
if (this.childRef.current) {
this.childRef.current.someMethod();
}
};

render() {
return (
<>
<ChildComponent ref={this.childRef} />
<button onClick={this.callChildMethod}>Call Child Method</button>
</>
);
}
}

在这个函数组件的例子中,ChildComponent 使用了 forwardRef 来接收来自父组件的 ref。然后,它使用 useImperativeHandle Hook 来定义当父组件使用 ref 调用方法时应该暴露什么方法。这样,ParentComponent 就可以像在类组件例子中那样调用子组件的方法了。

这种方法应当谨慎使用,并保持 React 数据流的单向性,在大多数情况下,应该尝试通过状态提升或者回调函数来解决问题,而不是直接调用子组件的方法。