2024-08-27 10:10:05 +08:00

568 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 3. *Ref*
关于 *Ref*,我们在前面入门篇实际上已经有所涉及了,当时通过 *Ref* 获取到 *markdown* 编辑器的 *DOM* 节点,然后获取用户所输入的文档内容。
这一讲,我们就来彻底看一下 *Ref*,包含以下的内容:
- 过时 *API**String* 类型的 *Refs*
- *createRef API*
- *Ref* 转发
- *useRef* 与 *useImperativeHandle*
## 过时 *API**String* 类型的 *Refs*
首先,我们还是需要认识到 *Ref* 是为了解决什么问题。我们都知道,现代前端框架的一大特点就是响应式,开发人员不需要再去手动操作 *DOM* 元素,只需要关心和 *DOM* 元素绑定的响应式数据即可。
但是有些时候,我们需要操作 *DOM* 元素,例如官方所列举的这几个场景:
- 管理焦点,文本选择或媒体播放
- 触发强制动画
- 集成第三方 *DOM*
在最最早期的时候,*React* 中 *Ref* 的用法非常简单,类似于 *Vue*,给一个字符串类型的值,之后在方法中通过 *this.refs.xxx* 就能够引用到。
示例如下:
```js
import React, { Component } from 'react'
export default class App extends Component {
clickHandle = () => {
console.log(this);
console.log(this.refs.inputRef);
this.refs.inputRef.focus();
}
render() {
return (
<div>
<input type="text" ref="inputRef"/>
<button onClick={this.clickHandle}>聚焦</button>
</div>
)
}
}
```
在上面的代码中,我们在 *input* 上面挂了一个 *ref* 属性,对应的值为 *inputRef*,之后查看组件实例,可以看到该组件实例中的 *refs* 里面就保存了该 *input**DOM* 元素。
![image-20221130135240603](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055241.png)
然后我们就可以像之前一样进行 *DOM* 元素的操作了。例如在上面的示例中我们进行了聚焦的操作。
但是这里需要注意两点:
- 避免使用 *refs* 来做任何可以通过声明式实现来完成的事情
-*API* 已经过时,可能会在未来的版本被移除,官方建议我们使用回调函数或 *createRef API* 的方式来代替
参阅官网 *https://zh-hans.reactjs.org/docs/refs-and-the-dom.html#legacy-api-string-refs*
至于为什么 *String* 类型的 *Refs* 会被废弃,主要是以下几个方面原因:
![image-20221130135301934](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055302.png)
参阅地址:*https://github.com/facebook/react/pull/8333#issuecomment-271648615*
## *createRef API*
接下来我们来看一下官方推荐的 *createRef API*
示例如下:
```js
import React, { Component } from 'react'
export default class App extends Component {
constructor(props) {
super();
this.inputRef = React.createRef();
console.log(this.inputRef); // {current: null}
}
clickHandle = () => {
console.log(this.inputRef); // {current: input}
this.inputRef.current.focus();
}
render() {
return (
<div>
<input type="text" ref={this.inputRef}/>
<button onClick={this.clickHandle}>聚焦</button>
</div>
)
}
}
```
在上面的代码中,我们创建 *Ref* 不再是通过字符串的形式,而是采用的 *createRef* 这个静态方法创建了一个 *Ref* 对象,并在组件实例上面新增了一个 *inputRef* 属性来保存这个 *Ref* 对象。
![image-20221130135404602](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055404.png)
*createRef* 这个方法本质也很简单,就是返回了一个 *{current: null}* 的对象,下面是 *createRef* 的源码:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055424.png" alt="image-20221130135424421" style="zoom:50%;" />
最后我们把这个对象和 *input* 进行关联。
如果要获取 *DOM* 元素,可以通过 *this.inputRef.current* 来获取。
除了在 *JSX* 中关联 *Ref*,我们还可以直接关联一个类组件,这样就可以直接调用该组件内部的方法。例如:
```js
// 子组件
import React, { Component } from 'react'
export default class ChildCom1 extends Component {
test = () => {
console.log("这是子组件的 test 方法");
}
render() {
return (
<div>ChildCom1</div>
)
}
}
```
```js
// 父组件
import React, { Component } from 'react';
import ChildCom1 from "./components/ChildCom1"
export default class App extends Component {
constructor(props) {
super();
this.comRef = React.createRef();
}
clickHandle = () => {
console.log(this);
console.log(this.comRef); // {current: ChildCom1}
this.comRef.current.test();
}
render() {
return (
<div>
{/* ref 关联子组件 */}
<ChildCom1 ref={this.comRef}/>
<button onClick={this.clickHandle}>触发子组件方法</button>
</div>
)
}
}
```
>虽然提供这种方式,但这是一种**反模式**,相当于回到了 *jQuery* 时代,因此尽量避免这么做。
*React.createRef API* 是在 *React 16.3* 版本引入的,如果是稍早一点的版本,官方推荐使用回调 *Refs*,也就是函数的形式。例如:
```js
import React, { Component } from 'react';
import ChildCom1 from "./components/ChildCom1"
export default class App extends Component {
constructor() {
super();
this.inputRef = element => {
this.inputDOM = element;
};
this.comRef = element => {
this.comInstance = element;
};
}
clickHandle = () => {
this.inputDOM.focus();
this.comInstance.test();
}
render() {
return (
<div>
{/* ref 关联子组件 */}
<input type="text" ref={this.inputRef} />
<ChildCom1 ref={this.comRef} />
<div>
<button onClick={this.clickHandle}>聚焦并且触发子组件方法</button>
</div>
</div>
)
}
}
```
你可能会好奇,为什么上面的例子都是使用的类组件,现在不都是使用函数组件了么?这是因为默认情况下,你不能在**函数组件上**使用 *ref* 属性,因为它们没有实例,但是在函数组件内部是可以使用 *ref* 的,这涉及到后面要说的 *useRef*
## *Ref* 转发
既然要讲 *Ref*,咱们就一起把它整个知识点一起讲完,接下来要介绍的是*Ref* 的转发。
*Ref* 转发是一个可选特性,其允许某些组件接收 *ref*,并将其向下传递(换句话说,“转发”它)给子组件。
那么什么时候需要 *Ref* 的转发呢?往往就在使用高阶组件的时候。
我们先来看一下如果没有 *Ref* 转发,在高阶组件中使用 *Ref* 会遇到什么问题。
```js
// App.jsx
import React, { Component } from 'react'
import withLogin from "./HOC/withLog";
import ChildCom1 from "./components/ChildCom1"
const NewChild = withLogin(ChildCom1);
export default class App extends Component {
constructor() {
super();
this.comRef = React.createRef();
this.state = {
show: true
}
}
clickHandle = () => {
// 查看当前的 Ref 所关联的组件
console.log(this.comRef);
}
render() {
return (
<div>
<button onClick={() => this.setState({
show: !this.state.show
})}>show/hide</button>
<button onClick={this.clickHandle}>触发子组件方法</button>
{this.state.show ? <NewChild ref={this.comRef} /> : null}
</div>
)
}
}
```
```js
// withLog.js
import { Component } from "react";
import { formatDate } from "../utils/tools";
// 高阶组件是一个函数,接收一个组件作为参数
// 返回一个新的组件
function withLog(Com) {
// 返回的新组件
return class extends Component {
constructor(props) {
super(props);
this.state = { n: 1 };
}
componentDidMount() {
console.log(
`日志:组件${Com.name}已经创建,创建时间${formatDate(
Date.now(),
"year-time"
)}`
);
}
componentWillUnmount() {
console.log(
`日志:组件${Com.name}已经销毁,销毁时间${formatDate(
Date.now(),
"year-time"
)}`
);
}
render() {
return <Com {...this.props} />;
}
};
}
export default withLog;
```
```js
// ChildCom1.jsx
import React, { Component } from 'react'
export default class ChildCom1 extends Component {
test = () => {
console.log("这是子组件的 test 方法");
}
render() {
return (
<div>ChildCom1</div>
)
}
}
```
在上面的三段代码中,我们使用了 *withLog* 这个高阶组件来包裹 *ChildCom1* 子组件,从而添加日志功能。在使用由高阶组件返回的增强组件时,我们传递了一个 *Ref*,我们的本意是想要这个 *Ref* 关联原本的子组件,从而可以触发子组件里面的方法。
但是我们会发现 *Ref* 关联的是高阶组件中返回增强组件,而非原来的子组件。
![image-20221130135500947](https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055501.png)
要解决这个问题就会涉及到 *Ref* 的转发。说直白一点就是 *Ref* 的向下传递给子组件。
这里 *React* 官方为我们提供了一个 *React.forwardRef API*。我们需要修改的仅仅是高阶组件:
```js
import React, { Component } from "react";
import { formatDate } from "../utils/tools";
// 高阶组件是一个函数,接收一个组件作为参数
// 返回一个新的组件
function withLog(Com) {
// 返回的新组件
class WithLogCom extends Component {
constructor(props) {
super(props);
this.state = { n: 1 };
}
componentDidMount() {
console.log(
`日志:组件${Com.name}已经创建,创建时间${formatDate(
Date.now(),
"year-time"
)}`
);
}
componentWillUnmount() {
console.log(
`日志:组件${Com.name}已经销毁,销毁时间${formatDate(
Date.now(),
"year-time"
)}`
);
}
render() {
// 通过 this.props 能够拿到传递下来的 ref
// 然后和子组件进行关联
const {forwardedRef, ...rest} = this.props;
return <Com ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
// 这里是关键,渲染函数会自动传入 ref然后我们将 ref 继续往下传递
return <WithLogCom {...props} forwardedRef={ref} />;
});
}
export default withLog;
```
在上面的代码中,*React.forwardRef* 接受一个渲染函数,该函数接收 *props**ref* 参数并返回原本我们直接返回的增强组件。
接下来我们在增强组件的 *render* 方法中,通过 *this.props* 拿到 *ref* 继续传递给子组件。
那么 *React.forwardRef* 究竟做了啥呢?源码如下:
<img src="https://xiejie-typora.oss-cn-chengdu.aliyuncs.com/2022-11-30-055526.png" alt="image-20221130135525552" style="zoom:50%;" />
可以看到,实际上 *forwardRef* 这个静态方法实际上也就是返回一个 *elementType* 的对象而已,该对象包含一个 *render* 方法,也就是我们在使用 *React.forwardRef* 时传入的渲染函数。
之所以要这么多此一举,是因为该渲染函数会自动传入 *props**ref*,关键点就在这里,拿到 *ref* 后,后我们就可以将 *ref* 继续往下面传递给子组件。
## *useRef* 与 *useImperativeHandle*
关于 *Ref* 这一块,最后要看一下的就是这两个 *Hook*
我们知道,现在整个 *React* 是函数组件大行其道,那么自然我们会遇到函数组件下如何进行 *Ref* 的关联。
在函数组件中,官方为我们提供了新的 *useRef* 这个 *Hook*来进行关联,但是也可以使用 *createRef API*,示例如下:
```js
import React from 'react';
function App() {
const [counter, setCounter] = React.useState(1);
const inputRef1 = React.createRef();
const inputRef2 = React.useRef();
console.log("inputRef1:", inputRef1); // {current: null}
console.log("inputRef2:", inputRef2); // {current: undefined}
function clickHandle() {
console.log("inputRef1:", inputRef1); // {current: input}
console.log("inputRef2:", inputRef2); // {current: input}
setCounter(counter + 1);
}
return (
<div>
<button onClick={clickHandle}>+1</button>
<div>{counter}</div>
<div>
<input type="text" ref={inputRef1} />
</div>
<div>
<input type="text" ref={inputRef2} />
</div>
</div>
);
}
export default App;
```
通过上面的示例我们可以看出,虽然 *createRef**useRef* 都是创建 *Ref* 的,但是还是有一些区别,主要体现在下面的点:
- *useRef* 是 *hooks* 的一种,一般用于 *function* 组件,而 *createRef* 一般用于 *class* 组件
-*useRef* 创建的 *ref* 对象在组件的整个生命周期内都不会改变,但是由 *createRef* 创建的 *ref* 对象,组件每更新一次,*ref*对象就会被重新创建
实际上,就是因为在函数式组件中使用 *createRef* 创建 *ref* 时存在弊端,组件每次更新,*ref* 对象就会被重新创建,所以出现了 *useRef* 来解决这个问题。
*useRef* 还接受一个初始值,这在用作关联 *DOM* 元素时通常没什么用,但是在作为**存储不需要变化**的全局变量时则非常方便。来看下面的例子:
```js
import { useState, useEffect } from 'react';
function App() {
let timer;
const [counter, setCounter] = useState(1);
useEffect(() => {
timer = setInterval(() => {
console.log('触发了');
}, 1000);
},[]);
const clearTimer = () => {
clearInterval(timer);
}
function clickHandle(){
console.log(timer);
setCounter(counter + 1);
}
return (
<>
<div>{counter}</div>
<button onClick={clickHandle}>+1</button>
<button onClick={clearTimer}>停止</button>
</>)
}
export default App;
```
上面的写法存在一个问题,如果这个 *App* 组件里有 *state* 变化或者他的父组件重新 *render* 等原因导致这个 *App* 组件重新 *render* 的时候,我们会发现,点击停止按钮,定时器依然会不断的在控制台打印,定时器清除事件无效了。
因为组件重新渲染之后,这里的 *timer* 以及 *clearTimer* 方法都会重新创建,*timer* 已经不是存储的之前的定时器的变量了。
此时根据 *useRef* 在组件的整个生命周期内都不会改变的特性,我们可以将定时器变量存储到 *useRef* 所创建的对象上面,示例如下:
```js
import { useState, useEffect, useRef } from 'react';
function App() {
let timer = useRef(null);
const [counter, setCounter] = useState(1);
useEffect(() => {
timer.current = setInterval(() => {
console.log('触发了');
}, 1000);
},[]);
const clearTimer = () => {
clearInterval(timer.current);
}
function clickHandle(){
console.log(timer);
setCounter(counter + 1);
}
return (
<>
<div>{counter}</div>
<button onClick={clickHandle}>+1</button>
<button onClick={clearTimer}>停止</button>
</>)
}
export default App;
```
最后,我们要看一下另外一个 *useImperativeHandle* 这个 *Hook*
*Hook* 一般配合 *React.forwardRef* 使用,主要作用是父组件传入 *Ref* 时,**自定义**要暴露给父组件的实例值。
来看一个具体的示例:
```js
import {useRef} from 'react';
import ChildCom1 from "./components/ChildCom1"
function App() {
const comRef = useRef();
function clickHandle(){
comRef.current.click();
}
return (
<div>
<ChildCom1 ref={comRef}/>
<button onClick={clickHandle}>触发子组件的方法</button>
</div>
);
}
export default App;
```
在父组件中,我们向子组件传递了一个 *Ref*,但是子组件实际上是一个函数组件。之前我们有说过,函数组件本身是无法挂 *Ref* 的,因此此时就需要使用 *React.forwardRef* 进行 *Ref* 的转发,之后配合 *useImperativeHandle* 来自定义要暴露给父组件的实例值。
```js
import React, { useRef, useImperativeHandle } from 'react';
function ChildCom1(props, ref) {
const childRef = useRef();
// 第一个是父组件传递过来的 ref
// 第二个回调函数返回一个对象,该对象是一个映射关系
// 映射关系中的键之后能够暴露给父组件使用
// 映射关系中的值对应的是对应的方法
useImperativeHandle(ref, () => ({
click: () => {
console.log(childRef.current);
}
}));
function clickHandle() {
console.log("这是子组件的 test 方法");
}
return (
<div onClick={clickHandle} ref={childRef}>
子组件1
</div>
);
}
// 需要做 ref 转发
export default React.forwardRef(ChildCom1);
```
在上面的代码中,我们使用了 *useImperativeHandle* 这个 *Hook*,该 *Hook* 的第一个参数是父组件传递进来的 *ref*,第二个回调函数返回一个对象,该对象是一个映射关系,映射关系中的键之后能够暴露给父组件使用,映射关系中的值对应的是对应的方法。
---
-*EOF*-