返回首页

说说对 React 的 refs 属性的理解?应用场景?

问题解析(面试官考察点)

面试官通过此问题主要考察:

  • 对 refs 概念和作用的理解
  • 对 refs 不同使用方式的掌握
  • 对 refs 适用场景的了解
  • 对 refs 使用注意事项的认识

核心概念(基础知识点)

什么是 Refs

Refs(references)提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。Refs 在以下场景中特别有用:

  1. 管理焦点、文本选择或媒体播放
  2. 触发强制动画
  3. 集成第三方 DOM 库
  4. 获取子组件的实例或 DOM 节点

Refs 的本质

Refs 本质上是一个对象,包含一个 current 属性,指向被引用的 DOM 节点或组件实例:

{
  current: DOMElement | ComponentInstance | null
}

详细解答(代码示例)

创建 Refs 的三种方式

1. createRef(类组件推荐)

import React, { Component } from "react";

class InputFocus extends Component {
  constructor(props) {
    super(props);
    // 创建 ref
    this.inputRef = React.createRef();
  }

  componentDidMount() {
    // 通过 current 访问 DOM 节点
    this.inputRef.current.focus();
  }

  handleClick = () => {
    // 获取 input 的值
    alert(this.inputRef.current.value);
  };

  render() {
    // 通过 ref 属性绑定
    return (
      <div>
        <input ref={this.inputRef} type="text" placeholder="输入内容" />
        <button onClick={this.handleClick}>获取值</button>
      </div>
    );
  }
}

2. useRef(函数组件推荐)

import React, { useRef, useEffect } from "react";

function InputFocus() {
  // 创建 ref
  const inputRef = useRef(null);

  useEffect(() => {
    // 组件挂载后聚焦
    inputRef.current.focus();
  }, []);

  const handleClick = () => {
    alert(inputRef.current.value);
  };

  return (
    <div>
      <input ref={inputRef} type="text" placeholder="输入内容" />
      <button onClick={handleClick}>获取值</button>
    </div>
  );
}

3. 回调 Refs

class CustomTextInput extends Component {
  constructor(props) {
    super(props);
    this.inputElement = null; // 用于存储 ref
  }

  componentDidMount() {
    // 直接访问 DOM 节点
    if (this.inputElement) {
      this.inputElement.focus();
    }
  }

  render() {
    return (
      <div>
        <input
          type="text"
          // 回调 ref DOM 节点赋值给实例属性
          ref={(element) => { this.inputElement = element; }}
        />
      </div>
    );
  }
}

// 回调 ref 也可以传递函数
function CustomTextInputWithCallback() {
  let inputElement = null;

  return (
    <div>
      <input
        type="text"
        ref={(element) => { inputElement = element; }}
      />
      <button onClick={() => inputElement && inputElement.focus()}>
        聚焦
      </button>
    </div>
  );
}

Refs 转发(forwardRef)

import React, { forwardRef } from "react";

// 子组件:使用 forwardRef 转发 ref
const FancyInput = forwardRef((props, ref) => {
  return (
    <div className="fancy-input">
      <input ref={ref} type="text" {...props} />
      <span className="icon"></span>
    </div>
  );
});

// 父组件
function Parent() {
  const fancyInputRef = useRef(null);

  const handleClick = () => {
    // 直接访问子组件内部的 input
    fancyInputRef.current.focus();
  };

  return (
    <div>
      <FancyInput ref={fancyInputRef} placeholder="请输入" />
      <button onClick={handleClick}>聚焦输入框</button>
    </div>
  );
}

// 在类组件中使用 forwardRef
const FancyButton = forwardRef((props, ref) => (
  <button ref={ref} className="fancy-button">
    {props.children}
  </button>
));

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

  render() {
    return (
      <FancyButton ref={this.buttonRef}>
        点击我
      </FancyButton>
    );
  }
}

useImperativeHandle(控制暴露内容)

import React, { useRef, forwardRef, useImperativeHandle } from "react";

// 使用 useImperativeHandle 控制暴露给父组件的方法
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null);

  // 只暴露特定的方法给父组件
  useImperativeHandle(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    },
    clear: () => {
      inputRef.current.value = "";
    },
    getValue: () => {
      return inputRef.current.value;
    }
    // 注意:没有暴露 inputRef.current 本身
  }));

  return <input ref={inputRef} type="text" {...props} />;
});

function Parent() {
  const fancyInputRef = useRef(null);

  const handleFocus = () => {
    fancyInputRef.current.focus();
  };

  const handleClear = () => {
    fancyInputRef.current.clear();
  };

  const handleGetValue = () => {
    alert(fancyInputRef.current.getValue());
  };

  return (
    <div>
      <FancyInput ref={fancyInputRef} />
      <button onClick={handleFocus}>聚焦</button>
      <button onClick={handleClear}>清空</button>
      <button onClick={handleGetValue}>获取值</button>
    </div>
  );
}

深入理解(原理剖析)

Refs 的工作原理

┌─────────────────────────────────────────────────────────────┐
│                     Refs 工作原理                            │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  1. 创建 Ref                                                  │
│     const myRef = useRef(null);                             │
│     // { current: null }                                    │
│                                                              │
│  2. 绑定到 JSX 元素                                          │
│     <input ref={myRef} />                                   │
│                                                              │
│  3. React 在渲染时处理 ref                                   │
│     - 组件挂载后:myRef.current = DOMElement               │
│     - 组件卸载后:myRef.current = null                     │
│                                                              │
│  4. 通过 current 访问                                        │
│     myRef.current.focus();                                  │
│                                                              │
└─────────────────────────────────────────────────────────────┘

Refs 的执行时机

class RefTiming extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
    console.log("constructor:", this.myRef.current); // null
  }

  render() {
    console.log("render:", this.myRef.current); // null
    return <div ref={this.myRef}>Content</div>;
  }

  componentDidMount() {
    console.log("componentDidMount:", this.myRef.current); // DOM 节点
  }

  componentWillUnmount() {
    console.log("componentWillUnmount:", this.myRef.current); // DOM 节点
  }
}

// 函数组件中的时机
function RefTimingFunction() {
  const myRef = useRef(null);

  console.log("render:", myRef.current); // 首次 null,之后 DOM 节点

  useEffect(() => {
    console.log("useEffect:", myRef.current); // DOM 节点
  });

  return <div ref={myRef}>Content</div>;
}

Refs 与闭包问题

function ClosureProblem() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  // 错误:ref 不会自动更新
  const handleClickWrong = () => {
    setTimeout(() => {
      alert(countRef.current); // 总是显示初始值 0
    }, 3000);
  };

  // 正确:手动更新 ref
  useEffect(() => {
    countRef.current = count;
  }, [count]);

  const handleClickCorrect = () => {
    setTimeout(() => {
      alert(countRef.current); // 显示最新的 count 值
    }, 3000);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={handleClickWrong}>Alert (Wrong)</button>
      <button onClick={handleClickCorrect}>Alert (Correct)</button>
    </div>
  );
}

最佳实践

1. 不要过度使用 Refs

// 不好的做法:用 ref 获取值,而不是受控组件
function BadExample() {
  const inputRef = useRef(null);

  const handleSubmit = () => {
    // 不推荐:直接从 DOM 获取值
    const value = inputRef.current.value;
    console.log(value);
  };

  return <input ref={inputRef} />;
}

// 好的做法:使用受控组件
function GoodExample() {
  const [value, setValue] = useState("");

  const handleSubmit = () => {
    console.log(value);
  };

  return (
    <input
      value={value}
      onChange={(e) => setValue(e.target.value)}
    />
  );
}

2. Refs 的适用场景

// 场景1:管理焦点
function FocusManager() {
  const firstInputRef = useRef(null);
  const secondInputRef = useRef(null);

  const handleFirstKeyDown = (e) => {
    if (e.key === "Enter") {
      secondInputRef.current.focus();
    }
  };

  return (
    <div>
      <input
        ref={firstInputRef}
        onKeyDown={handleFirstKeyDown}
        placeholder="按 Enter 跳到下一个"
      />
      <input ref={secondInputRef} placeholder="第二个输入框" />
    </div>
  );
}

// 场景2:媒体控制
function VideoPlayer() {
  const videoRef = useRef(null);

  const play = () => videoRef.current.play();
  const pause = () => videoRef.current.pause();

  return (
    <div>
      <video ref={videoRef} src="video.mp4" />
      <button onClick={play}>播放</button>
      <button onClick={pause}>暂停</button>
    </div>
  );
}

// 场景3:动画集成
function AnimationBox() {
  const boxRef = useRef(null);

  const animate = () => {
    // 使用第三方动画库
    gsap.to(boxRef.current, {
      x: 100,
      duration: 1
    });
  };

  return (
    <div>
      <div ref={boxRef} className="box" />
      <button onClick={animate}>动画</button>
    </div>
  );
}

// 场景4:测量 DOM 尺寸
function MeasureElement() {
  const elementRef = useRef(null);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  useEffect(() => {
    if (elementRef.current) {
      const { width, height } = elementRef.current.getBoundingClientRect();
      setDimensions({ width, height });
    }
  }, []);

  return (
    <div>
      <div ref={elementRef}>需要测量的内容</div>
      <p>Width: {dimensions.width}, Height: {dimensions.height}</p>
    </div>
  );
}

3. 避免在渲染期间访问 ref

function BadExample() {
  const myRef = useRef(null);

  // 错误:在渲染期间访问 ref
  if (myRef.current) {
    myRef.current.focus();
  }

  return <input ref={myRef} />;
}

function GoodExample() {
  const myRef = useRef(null);

  // 正确:在 effect 中访问
  useEffect(() => {
    if (myRef.current) {
      myRef.current.focus();
    }
  }, []);

  return <input ref={myRef} />;
}

面试要点

  1. Refs 是什么: 提供访问 DOM 节点或组件实例的方式

  2. 创建方式:

    • React.createRef()(类组件)
    • useRef()(函数组件)
    • 回调 refs
  3. 转发 Refs: 使用 forwardRef 将 ref 传递给子组件

  4. 控制暴露: 使用 useImperativeHandle 控制子组件暴露的内容

  5. 适用场景:

    • 管理焦点、文本选择、媒体播放
    • 触发强制动画
    • 集成第三方 DOM 库
    • 测量 DOM 尺寸
  6. 注意事项:

    • 不要过度使用 refs,优先使用受控组件
    • 不要在渲染期间访问 ref.current
    • 组件卸载后 ref.current 变为 null