Yapei Li

专注于前端领域

0%

Refs 和 DOM

Refs 和 DOM

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

在常规的 React 数据流中,props 是父组件与子组件交互的唯一方式。要修改子元素,你需要用新的 props 去重新渲染子元素。

Refs强制修改子元素

被修改的子元素可以是 React组件实例(类组件实例),或者是一个 DOM 元素不能使函数组件

何时使用 Refs

  1. 处理focus文本选择或者媒体播放
  2. 触发强制动画
  3. 集成第三方DOM库
  4. 如果可以通过声明式实现,就尽量避免使用 refs

不要过度使用 Refs

能使用状态提升运用state改变组件状态,不要使用 refs 来更新组件

创建 Refs: React.createRef()

使用 React.createRef() 创建 refs,通过 ref 属性来获得 子元素。

当构造组件时,refs 通常被赋值给实例的一个属性,这样你可以在组件中任意一处使用它们.

1
2
3
4
5
6
7
8
9
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}

访问 Refs: ref.current

ref 属性被传递给一个 render 函数中的元素时,可以使用 ref 中的 current 属性对节点的引用进行访问。

1
const node = this.myRef.current;

ref的值取决于节点的类型:

  1. ref 属性被用于一个普通的 HTML 元素时,React.createRef() 将接收底层 DOM 元素作为它的 current 属性以创建 ref
  2. ref 属性被用于一个自定义类组件时,ref 对象将接收该组件已挂载的实例作为它的 current
  3. 不能在函数式组件上使用 ref 属性,因为它们没有实例

React 组件在加载时将 DOM 元素传入 ref 的回调函数,在卸载时则会传入 null。在 componentDidMountcomponentDidUpdate 这些生命周期回调之前执行 ref 回调

在 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
31
32
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
// 1、创建一个ref来存储textInput DOM元素
this.textInput = React.createRef();

this.focusTextInput = this.focusTextInput.bind(this);
}

focusTextInput() {
// 使用原始DOM API操作文本获得焦点
//3、 通过访问“current”获取DOM节点
this.textInput.current.focus();
}

render() {
return (
<div>
//2、 使input与 ref 关联
<input
type="text"
ref={this.textInput} />
// 调用函数
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}

类(Class)组件 添加Ref

包装上面的 CustomTextInput ,来模拟挂载之后立即被点击:使用 ref 来访问自定义组件,并手动调用它的 focusTexInput 方法

这种方法仅对以类(class)声明的 CustomTextInput 有效

当ref是类组件实例时,可以直接获取实力上的属性及方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//上面的CustomTextInput组件不再重复写

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

componentDidMount() {
//获得自定义组件,并调用它的实例上的方法
this.textInput.current.focusTextInput();
}

render() {
return (
<CustomTextInput ref={this.textInput} />
);
}
}

Refs 与 函数式组件

不能在函数式组件上使用 ref 属性,因为它们没有实例,如果需要使用 ref ,就需要将组件转化成 类(class)组件,就像需要 生命周期方法 或者 state 一样。

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

class Parent extends React.Component {
constructor(props) {
super(props);
this.textInput = React.createRef();
}
render() {
// This will *not* work!
return (
<MyFunctionalComponent ref={this.textInput} />
);
}
}

但是可以 在函数式组件内部使用 ref 来引用一个 DOM 元素或者 类(class)组件:ref={(input) => { textInput = input; }}input就是个形参 代表当前元素,可以是任何形参名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function CustomTextInput(props) {
// textInput必须在这里声明,所以 ref 回调可以引用它
let textInput = null;

function handleClick() {
textInput.focus();
}

return (
<div>
<input
type="text"
ref={(input) => { textInput = input; }} />
//上边ref中的input就是个形参 代表当前元素,可以是任何形参名
<input
type="button"
value="Focus the text input"
onClick={handleClick}
/>
</div>
);
}

对父组件暴露 DOM 节点

通常不建议从父组件访问子节点的 DOM 节点,因为它会破坏组件的封装,但偶尔也可用于触发焦点测量子 DOM 节点的大小或位置

可以像上边 “类(Class) 组件添加 Ref”章节那样 先通过refs获得自定义组件,然后在组件内添加refs来实现

缺陷:只能获取组件实例不是 DOM 节点。并且,它还在函数式组件上无效

通常通过“Ref转发”实现对父组件暴露DOM节点

回调 Refs(能用 createRef不用回调refs)ref={element=>this.textInput = element}

“回调 ref”,更加细致地控制何时 ref 被设置和解除。

不同于传递 createRef() 创建的 ref 属性,它会传递一个函数。这个函数接受 React 组件的实例HTML DOM 元素作为参数,以存储它们并使它们能被其他地方访问。

回调ref``不需要通过 current属性访问,直接this.textInput.focus();

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
34
35
36
37
38
39
40
41
class CustomTextInput extends React.Component {
constructor(props) {
super(props);

this.textInput = null;

this.setTextInputRef = element => {
this.textInput = element;
};

this.focusTextInput = () => {
// 直接使用原生 API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
}

componentDidMount() {
// 渲染后文本框自动获得焦点
this.focusTextInput();
}

render() {
// 使用 `ref` 的回调将 text 输入框的 DOM 节点存储到 React
// 实例上(比如 this.textInput)
return (
<div>
<input
type="text"
ref={this.setTextInputRef}
/>
//相当于
//'ref={element=>this.textInput = element}'
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}

在组件间通过props传递回调形式的 refs(父组件获取子组件元素)

Parent 通过propsref 回调函数作为 inputRef属性 传递给 CustomTextInput组件,然后 CustomTextInput组件 通过 ref属性将其传递给 <input>元素。

最终,Parent 中的 this.inputElement 将被设置为与 CustomTextIput组件 中的 <input> 元素相对应的 DOM 节点

1
2
3
4
5
6
7
8
function CustomTextInput (props) {
return (
<div>
<input type="text" ref={props.inputRef}/>
</div>
)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Parent extends React.Component{
this.textInput=null; //写不写都行
click=()=>{ //不用属性初始化语法就必须bind(this)
this.inputElement.focus();
}
render(){
return(
<div>
<CustomTextInput inputRef={el => this.inputElement = el}></CustomTextInput>
<input type="button" value="sasasa" onClick={this.click} />
</div>

)
}
}

转发 Refs

Ref 转发是一种自动将 ref 通过组件传递给子组件的技术

Ref 转发是一种选择性加入的功能,可让某些组件接收他们收到的 ref,并将其向下传递(换句话说,“转发”)给子组件。

React.forwardRef((props,ref)=>())接收props,ref作为参数

第二个 ref 参数仅在使用 React.forwardRef 调用定义组件时才存在,常规函数或类组件不接收 ref 参数

Ref 转发不限于 DOM 组件。也可以将 refs 转发给类组件实例

FancyButton 使用 React.forwardRef 来获取传递给它的 ref , 然后将其转发给它渲染的的 DOM button:

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
34
35
36
37
38
39
//3. ref转发
const FancyButton =React.forwardRef((props,ref)=>(
<button ref={ref} className="FancyButton">
{props.children}
</button>
))

class Parent extends React.Component{
constructor(props){
super(props);
//1. 创建 ref
this.ref=React.createRef();
this.btnClick=this.btnClick.bind(this);
}
btnClick(){
//按理说ref为FancyButton组件
//但是ref转发以后 就变为了<button class="FancyButton">点我</button>
console.log(this.ref);
}
render(){
return (
<div>
//2. 绑定ref
<FancyButton ref={this.ref}>
我是你绑定的ref
</FancyButton>
<input type="button" value="打印ref" onClick={this.btnClick} />
</div>

)
}
}

ReactDOM.render(
<Parent></Parent>,
document.getElementById('root')
);

//打印结果 <button class="FancyButton">点我</button>

上面示例的步骤:

  1. 我们通过调用 React.createRef 创建一个 React ref 并将其分配给 ref 变量。

  2. 通过将 ref 变量传递给指定refJSX 属性的 <FancyButton ref={ref}>

  3. Reactref传递给 forwardRef 中的 (props, ref) => ... 函数作为第二个参数

  4. 我们将这个ref参数转发到指定ref JSX 属性的 <button ref = {ref}>

  5. 当附加 ref 时,ref.current 将指向 <button> DOM节点。

将父组件通过createRef创建的Ref通过props传递给子组件,子组件将传递过来的ref绑定在其元素上也能实现 ref转发

也可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}

class Parent extends React.Component {
constructor(props) {
super(props);
this.inputElement = React.createRef();
}
render() {
return (
<CustomTextInput inputRef={this.inputElement} />
);
}
}
/>

// 现在您可以根据需要设置焦点。
this.inputElement.current.focus();

当使用 HOC 扩展组件时,建议使用 ReactforwardRef 函数 将ref转发到 包装组件上

第三方 HOC 没有实现 ref 转发, 上述模式仍然可以用作后备。