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}>
    </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 (
        <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 (
        <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 (
            <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 (
        <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 (
        <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 (
        <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 (
        {</* 将 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 (
      <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 (
        <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 (
        <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 (
        <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 (
      <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 (
      <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 (
      <h2>{count}</h2>
      <input type="number" value={delay} onChange={handleChange} />
    </div>

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

受控组件和非受控组件

如果一个表单元素的值是由 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>
                    一般会使用 ref 获取 DOM 元素。例如:constructor(){    super();    // 创建 ref    this.divRef = React.createRef();}componentDidMount(){    // DOM 元素可以通过 current 属性获得    console.log(this.divRef.current);}render(){    // 使用 ref    return &lt;div ref={this.divRef
				
RefReference 的缩写,就是引用的意思class 组件使用 createRef 创建 Ref 实例: function 组件使用 useRef HOOK 声明 RefRef 与 render ref.current 发生变化不会造成 re-render不应该在 render 阶段更新 ref.current 属性,ref.current 发生变化应该作为 Side Effect(影响下次渲染)Reactjs 并不会跟踪 ref.current 的变化,不要把 ref.current 作为
使用 useEffect 添加 ref 作为依赖项会监听不到变化的原因是,在依赖项列表中的每个项目都会被浅比较,并且如果发现有任何项目发生变化,就会重新运行 effect。然而,对于 ref 类型的变量,每次浅比较的结果都是相同的,因为它们的引用地址始终相同。因此,即使 ref 指向的变量值发生了变化,也不会在依赖项列表中触发更新。 为了解决这个问题,可以使用 useRef 来跟踪 ref 指向的...
🤔 如何用useEffect模拟componentDidMount生命周期? 🤔 如何正确地在useEffect里请求数据?[]又是什么? 🤔 我应该把函数当做effect的依赖吗? 🤔 为什么有时候会出现无限重复请求的问题? 🤔 为什么有时候在effect里拿到的是旧的state或prop?
目录一、useEffect副作用 (1)使用useEffect(2)执行时机(第二个参数)(3)倒计时demo(4)请求ajax(5)useEffect的清理函数(6)图片移动demo(7)图片移动demo 封装二、 useRef(引用)(1)使用方法(2)demo 获取input的值(3)demo 清除定时器(4)demo 验证码倒计时(5)demo 验证码倒计时 抽离hooks三、useContext(跨组件传值)一、使用方法二、传值Demo (2)执行时机(第二个参数) 情况一:不写第二个参数
{/* 方式一 */} <div ref="div"></div> <button className="btn btn-primary" onClick={()=>this.send()}>操作DOM元素</button> send(){ // console.log(this.refs.div); this.refs.div.