专栏首页多云转晴React Ref 使用总结

React Ref 使用总结

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

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。例如:

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 属性不会引发组件重新渲染。比如下面函数组件的例子:

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 一样的功能:

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,例如:

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 还可以获取组件实例。例如:

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 节点

例如:

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。比如:

// 使用 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 元素。例如:

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 包一层。例如:

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 中的代码改一行即可:

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

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

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 按钮时停止计时,如何实现?

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

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。代码如下:

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,延迟时间会随着输入值不不同而变化。代码如下:

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。代码如下:

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 属性。例如下面的代码就是一个非受控组件。

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/

本文分享自微信公众号 - Neptune丶(Neptune_mh_0110),作者:多云转晴

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-08-29

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 【React】你想知道的关于 Refs 的知识都在这了

    Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

    Nealyang
  • 小结React(三):state、props、Refs

    在React中state、props、Refs都是最基础的概念,本文将同时梳理下这三个知识点,主要内容包括:

    前端林子
  • 关于ref的一切

    所以,React需要持续追踪当前render的组件。这会让React在性能上变慢。

    公众号@魔术师卡颂
  • [实战] 为了学好 React Hooks, 我抄了 Vue Composition API, 真香

    前几篇文章都在讲 React 的 Concurrent 模式, 很多读者都看懵了,这一篇来点轻松的,蹭了一下 Vue 3.0 的热度。讲讲如何在 React 下...

    _sx_
  • [译] 对比 React Hooks 和 Vue Composition API

    原文:https://dev.to/voluntadpear/comparing-react-hooks-with-vue-composition-api-4b...

    江米小枣
  • react源码解析5.jsx&核心api

    一句话概括就是,用js对象表示dom信息和结构,更新时重新渲染更新后的对象对应的dom,这个对象就是React.createElement()的返回结果

    全栈潇晨
  • 【React】:Refs

    Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。

    WEBJ2EE
  • 浅谈 React Refs

    在React组件中,props是父组件与子组件的唯一通信方式,但是在某些情况下我们需要在props之外强制修改子组件或DOM元素,这种情况下React提供了Re...

    IMWeb前端团队
  • React的Refs方法获取DOM实例 和 访问子组件方法及属性

    React 支持一种非常特殊的属性 Ref ,你可以用来绑定到 render() 输出的任何组件上。

    小弟调调

扫码关注云+社区

领取腾讯云代金券