前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React Ref 使用总结

React Ref 使用总结

作者头像
多云转晴
发布2020-09-08 10:54:26
6.9K0
发布2020-09-08 10:54:26
举报
文章被收录于专栏:webTowerwebTower

在 React 程序中,一般会使用 ref 获取 DOM 元素。例如:

代码语言:javascript
复制
constructor(){
    super();
    // 创建 ref
    this.divRef = React.createRef();
}
componentDidMount(){
    // DOM 元素可以通过 current 属性获得
    console.log(this.divRef.current);
}
render(){
    // 使用 ref
    return <div ref={this.divRef}>123</div>
}

使用 refs 的几个场景:

  • 管理焦点,文本选择或媒体播放;
  • 触发强制动画;
  • 集成第三方 DOM 库;

在 React Hook 中可以使用 useRef 创建一个 ref。例如:

代码语言:javascript
复制
function App(){
  let divRef = useRef();

  useEffect(() => {
    // 渲染完成后获取 DOM 元素
    console.log(divRef.current);
  },[]);

  return (
    <div ref={divRef}>
      123
    </div>
  );
}

useRef 还可以传入一个初始值,这个值会保存在 ref.current 中,上面代码中,如果不给 div 元素传递 ref={divRef},则 divRef.current 的值将是我们传入的初始值。

useRefcreateRef 并没有什么区别,只是 createRef 用在类组件当中,而 useRef 用在 Hook 组件当中。在类组件中,可以在类的实例上存放内容,这些内容随着实例化产生或销毁。但在 Hook 中,函数组件并没有 this(组件实例),因此 useRef 作为这一能力的弥补。在组件重新渲染时,返回的 ref 对象在组件的整个生命周期内保持不变。变更 ref 对象中的 .current 属性不会引发组件重新渲染。比如下面函数组件的例子:

代码语言:javascript
复制
function App(){
  let uRef = useRef(1);
  let [count, setCount] = useState(uRef.current);

  const handleClick = useCallback(() => {
    uRef.current += 1;  // current 值加一
    setCount(uRef.current);
  },[]);

  return (
    <div>
        <h1>useRef: { count }</h1>
        <button onClick={handleClick}>Click!</button>
    </div>
  );
}

上面代码中,每次点击按钮 uRef.current 就会加一,并更新 count 值。count 值会一直累加,如果把 h1 中的 count 换成 uRef.current,组件并不会更新。当然,如果给 useCallback 的数组中添加 uRef.current,让它监听其变化,那还是会更新的,但不应这么做。这就失去了 ref 的意义。

不要在 Hook 组件(或者函数组件)中使用 createRef,它没有 Hook 的功能,函数组件每次重渲染,createRef 就会生成新的 ref 对象。使用类组件实现上面 Hook 一样的功能:

代码语言:javascript
复制
class App extends Component{
  constructor(){
    super();
    this.state = {
      count: 1
    };
    this.divRef = React.createRef();
    this.divRef.current = 1;    // 初始化
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick(){
    this.divRef.current += 1;
    this.setState({
      count: this.divRef.current
    });
  }
  render () {
    return (
      <div>
        <h1>Hello! {this.state.count}</h1>
        <button onClick={this.handleClick}>Click</button>
      </div>
    );
  }
}

createRefuseRef 出现之前,可以使用回调的方式使用 ref 获取 DOM,例如:

代码语言:javascript
复制
class App extends Component{
    constructor(){
        super();
        this.iptRef = null;
    }
    componentDidMount(){
        // 组件挂载完成后,输入框自动对焦
        this.iptRef.focus();
    }
    render () {
        return (
        <div>
            <input type="text" ref={ipt => this.iptRef = ipt} />
        </div>
        );
    }
}

上面代码中,元素的 ref 接受一个函数,函数的参数就是 DOM 节点,然后把节点赋给组件实例的 iptRef

其他 DOM 操作场景

在组件上使用 ref

上面介绍了如何在 DOM 元素上使用 ref,ref 还可以获取组件实例。例如:

代码语言:javascript
复制
class Counter extends Component{
  constructor(){
    super();
    this.state = {
      count: 1
    }
    this.increment = this.increment.bind(this);
  }
  increment(){
    this.setState({
      count: this.state.count + 1
    });
  }
  render(){
    return (
      <div>
        <h1>count: {this.state.count}</h1>
      </div>
    );
  }
}

class App extends Component{
  constructor(){
    super();
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick () {
    this.counterIntance.increment();
  }
  render () {
    return (
      <div>
        <Counter ref={obj => this.counterIntance = obj} />
        <button onClick={this.handleClick}>Click</button>
      </div>
    );
  }
}

在 App 组件中,Counter 子组件使用 ref 获取其实例对象,父组件用 counterIntance 属性接收。当点击按钮时会调用 Counter 组件上的 increment 方法。

父组件访问子组件的 DOM 节点

例如:

代码语言:javascript
复制
function Input(props){
  return (
    <input type="text" ref={ props.iptRef } />
  );
}

class App extends Component{
  componentDidMount(){
    console.log(this.iptElm);
  }
  render () {
    return (
      <div>
        <Input iptRef={el => this.iptElm = el} />
      </div>
    );
  }
}

将父组件的 iptRef 状态(是一个 ref 回调形式的函数)传递给子组件,父组件中的 iptElm 就可以接收到 DOM 元素了。如果不使用 Hook,在函数组件中是无法操作 DOM 的,一个办法就是写成类组件形式,或者将 DOM 元素传递给父组件(父组件应是一个类组件)。除了使用这种方式外,也可以使用 React 提供的 forwardRef API。比如:

代码语言:javascript
复制
// 使用 forwardRef 包裹后,函数组件的第二个参数将是,父组件传入的 ref 对象
const Input = React.forwardRef((props, iptRef) => {
  return (
    <input type="text" ref={iptRef} />
  );
});

class App extends Component{
  constructor(){
    super();
    // 创建 ref 对象
    this.iptRef = createRef();
  }
  componentDidMount(){
    // 将会打印出 input 元素
    console.log(this.iptRef.current);
  }
  render () {
    return (
      <div>
        {</* 将 ref 对象传入子组件当中 */}
        <Input ref={this.iptRef} />
      </div>
    );
  }
}

对于高阶组件(HOC),可以利用 forwardRef 实现父组件获取子组件 DOM 元素。例如:

代码语言:javascript
复制
function withComp(WrapperComponent){
  class Example extends Component{
    render(){
      const { forwardRef, ...rest } = this.props;
      return <WrapperComponent ref={forwardRef} {...rest}  />
    }
  }
  return React.forwardRef((props, ref) => {
    return <Example {...props} forwardRef={ref} />
  });
}

withComp 是一个高阶组件,它会返回 forwardRef 包裹的函数组件,这个函数组件内部直接返回 Example 类组件,使用 forwardRef 属性接收到从父组件传来的 ref 对象。Example 组件中就可以接收到函数组件传递来的 forwardRef 属性,然后 WrapperComponent 相当于父组件,我们自己写的子组件需要使用 forwardRef 包一层。例如:

代码语言:javascript
复制
const Child = React.forwardRef((props, forwardRef) => {
  return (
    <div>
      <h1>{props.msg}</h1>
      {/* forwardRef 是父组件传来的 ref 对象 */}
      <input ref={forwardRef} type="text" />
    </div>
  );
});

// 增强组件
const Input = withComp(Child);

class App extends Component{
  constructor(){
    super();
    this.state = {
      msg: 'Hello',
    };
    this.iptRef = createRef();
  }
  componentDidMount(){
    // 获取到 input 元素
    console.log(this.iptRef.current);
  }
  render(){
    return (
      <div>
        <Input ref={ this.iptRef } msg={ this.state.msg } />
      </div>
    );
  }
}

如果你不想用 React.forwardRef “包一层”,也可以交由高阶组件来完成,把 withComp 中的代码改一行即可:

代码语言:javascript
复制
class Example extends Component{
    render(){
      const { forwardRef, ...rest } = this.props;
      // forwardRef 作为属性传给 WrapperComponent(Child组件)
      return <WrapperComponent forwardRef={forwardRef} {...rest}  />
    }
}

ref 对象传递给 WrapperComponent 组件。这样,我们在子组件中使用 ref 时直接使用即可:

代码语言:javascript
复制
function Child(props) {
    // 此时父组件传来的 ref 对象在 props 中
    // 不好的一点是,只能使用 props.forwardRef 获取
    // 这可能会出现问题:父组件中传入的就有 forwardRef 属性,
    // 值就会被覆盖或者获取到的不是 ref 对象
    return (
        <div>
        <h1>{props.msg}</h1>
        {/* 从 props 中取出 forwardRef,即 ref 对象 */}
        <input ref={props.forwardRef} type="text" />
        </div>
    );
};
const Input = withComp(Child);
// ....

使用 useRef

useRef 除了访问 DOM 节点外,useRef 还可以有别的用处,你可以把它看作类组件中声明的实例属性,属性可以存储一些内容,内容改变不会触发视图更新。以一个计时器的例子了解 useRef 的用法。

Demo 描述:一个 100ms 的计时器,当点击 Start 按钮时就会计时,点击 End 按钮时停止计时,如何实现?

如果使用类组件,可以这么做:

代码语言:javascript
复制
class App extends React.Component{
  constructor(){
    super();
    this.state = {
      count: 0
    };
    // 用于接收定时器 ID
    this.timer = undefined;
    this.startHandler = this.startHandler.bind(this);
    this.endHandler = this.endHandler.bind(this);
  }
  startHandler(){
    if(!this.timer){  // 如果定时器没有值时才去赋值,不然多次点击按钮会设置多个定时器
      this.timer = setInterval(() => {
        this.setState({
          count: this.state.count + 1
        });
      },100);
    }
  }
  endHandler(){
    clearInterval(this.timer);
    // 把定时器设置成假值
    this.timer = undefined;
  }
  render(){
    return (
      <div>
        <h2>{this.state.count}</h2>
        <button onClick={this.startHandler}>Start!</button>
        <button onClick={this.endHandler}>Stop!</button>
      </div>
    );
  }
}

在类组件中,可以定义一个 timer 属性用于接收定时器 ID,但在函数组件中并没有 this(组件实例),这就要借助到 useRef。代码如下:

代码语言:javascript
复制
function App(){
  let [count, setCount] = useState(0);
  const timer = useRef(null);

  const start = useCallback(() => {
    if(timer.current)   return;
    timer.current = setInterval(() => {
      setCount(count => count + 1);
    },100);
  },[]);
  const stop = useCallback(() => {
    clearInterval(timer.current);
    timer.current = null;
  },[]);

  return (
    <div>
      <h2>{count}</h2>
      <button onClick={start}>Start!</button>
      <button onClick={stop}>Stop!</button>
    </div>
  );
}

可以看到,使用函数组件要比类组件书写简洁许多。

再看一个例子,实现一个下面动图这样的功能,输入框输入的数字相当于计时器的毫秒延迟,当输入框数值变化时计时器会做相应的调整。如何实现?

显然,我们需要两个状态,一个是 count,表示数字的变化;另一个是 delay,延迟时间会随着输入值不不同而变化。代码如下:

代码语言:javascript
复制
function App(){
  let [count, setCount] = useState(0);
  let [delay, setDelay] = useState(1000);
  const timer = useRef(null);

  useEffect(() => {
    if(!timer.current){
      timer.current = setInterval(() => {
        setCount(count => count + 1);
      },delay);
    }
    return () => {
      clearInterval(timer.current);
      timer.current = null;
    }
  },[delay]);

  const handleChange = useCallback((event) => {
    setDelay(event.target.value);
  },[]);

  return (
    <div>
      <h2>{count}</h2>
      <input type="number" value={delay} onChange={handleChange} />
    </div>
  );
}

我们可以把中间的 useEffect 部分抽离出来,自定义一个 Hook:useInterval。代码如下:

代码语言:javascript
复制
const useInterval = (callback, delay) => {
  const savedCallback = useRef();

  useEffect(() => {   // callback 变更时重新赋值
    savedCallback.current = callback;
  },[callback]);

  useEffect(() => {
    // 每次 delay 变化(重渲染),都应生成一个新的计时器回调
    // 这样计时器的回调函数才会引用新的 props 和 state
    const handler = () => savedCallback.current();

    if(delay !== null){
      const id = setInterval(handler, delay);
      return () => clearInterval(id);   // 别忘了清除计时器
    }
  },[delay]);
}

function App(){
  let [count, setCount] = useState(0);
  let [delay, setDelay] = useState(1000);

  useInterval(function(){
    setCount(count + 1);
  },delay);

  const handleChange = useCallback((event) => {
    setDelay(event.target.value);
  },[]);

  return (
    <div>
      <h2>{count}</h2>
      <input type="number" value={delay} onChange={handleChange} />
    </div>
  );
}

关于 useInterval 介绍可以参考这篇文章:使用 React Hooks 声明 setInterval[1]

受控组件和非受控组件

如果一个表单元素的值是由 React 控制,就其称为受控组件。比如 input 框的 value 由 React 状态管理,当 change 事件触发时,改变状态。而非受控组件就像是运行在 React 体系之外的表单元素,当用户将数据输入到表单字段(例如 input,dropdown 等)时,React 不需要做任何事情就可以映射更新后的信息,非受控组件可能就要手动操作 DOM 元素(使用 React 中的 ref 获取元素),input 中使用 defaultValue 取代 value 属性,defaultChecked 代替 checked 属性。例如下面的代码就是一个非受控组件。

代码语言:javascript
复制
class App extends React.Component{
  constructor(){
    super();
    this.iptRef = React.createRef();
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  handleSubmit(event){
    // 提交时获取到输入框的值
    console.log(this.iptRef.current.value);
    // ... 做一些表单信息的验证操作
    event.preventDefault();
  }
  render(){
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="text" defaultValue="" ref={this.iptRef} />
        <input type="submit" value="提交" />
      </form>
    )
  }
}

参考资料

[1]

使用 React Hooks 声明 setInterval: https://overreacted.io/zh-hans/making-setinterval-declarative-with-react-hooks/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-08-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 WebTower 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 其他 DOM 操作场景
    • 在组件上使用 ref
      • 父组件访问子组件的 DOM 节点
      • 使用 useRef
      • 受控组件和非受控组件
        • 参考资料
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档