likes
comments
collection
share

【前端小课堂】—— React中Refs

作者站长头像
站长
· 阅读数 61

一、关于Refs的概念

Refs是什么?用来做什么?

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

在React中,提倡数据驱动视图的更新模式。

父组件与子组件最理想的通信方式是,通过子组件的props把数据传入子组件内部;子组件通过事件把组件内部数据,传出给父组件。从而实现数据的内外同步,进而实现组件状态的改变,最后反映到视图的更新。

在这种模式下,视图会跟随数据逻辑的变化,而自动更新。开发者可以把更多的精力放在数据逻辑上面。

【前端小课堂】—— React中Refs

在极少数情况下,我们可能需要强制修改元素或者修改元素的属性。比如,input获取焦点;DOM元素附加样式等。

为了解决此类问题,React引入了Refs相关api,用于访问目标元素。目标元素可以是DOM元素或者组件实例。

为了书写方便,下面我们一致使用目标元素来代替“DOM元素或者组件实例”。

二、创建Refs的几种方式

1、使用createRef创建Refs

通常使用 createRef() 创建ref对象,进而把ref对象赋值给目标元素的ref属性,然后通过ref.current可以访问到目标元素。

class Test extends React.Component {
  constructor(props) {
    super(props);
    this.testRef = React.createRef()
  }
  
  log(){
      console.log(this.testrRef.current.value)
  }
  
  render() {
    return <input ref={this.testRef} />
  }
}

2、使用回调形式的Refs

上面的例子使用在 React 16.3 版本引入的 createRef() API创建ref对象。较早版本的 React,可以使用回调形式的 Refs,创建ref对象。

React在组件挂载时,会调用 ref 回调函数并传入目标元素,当卸载时再次调用并传入null。而且在 componentDidMount 或 componentDidUpdate 触发前都会调用回调函数,以保证 ref对象绑定的目标对象是最新的。

我们推荐使用这种方式创建 ref对象。

需要注意,回调函数直接引用了目标元素,不需要current属性访问。

class Test extends React.Component {
  constructor(props) {
    super(props);

    this.setRef = (target) => {
      this.testRef = target;
    };
  }

  log() {
    console.log(this.testRef?.value);
  }

  render() {
    return (
      <div>
        <input defaultValue="abc" ref={this.setRef} />
        <button onClick={() => this.log()}>显示ref</button>
      </div>
    );
  }
}

3、使用字符串声明Refs

如果ref属性值是字符串,React会自动创建函数,处理ref逻辑。这种方式创建的ref对象,会自动保存到this.refs上面。我们可以通过this.refs.refName访问到ref对象。

官方已经不建议使用这个方式,未来可能会被移除。

class TestComponent extends React.Component {
  constructor(props) {
    super(props);
  }

  log() {
    console.log(this.refs.testRef.value);
  }

  render() {
    return (
      <div>
        <input defaultValue="abc" ref='testRef' />
        <button onClick={() => this.log()}>显示ref</button>
      </div>
    );
  }
}

4、使用useRef

class组件中,使用createRef()语法,创建ref对象。但createRef()语法并不适用于函数组件。 因为createRef方法的功能,只是创建并返回一个带current属性的对象。如果函数组件中使用createRef(),每次渲染都会重新创建一个新的对象,无法保留最初创建的ref对象。

参考createRef方法的源码:

export function createRef() {
  const refObject = {
    current: null,
  }

  return refObject
}

为了解决这个问题,React官方引入了useRef hook,用于函数组件中创建ref对象。使用useRef()创建的ref对象,和函数组件对应的 fiber 对象关联。因此,ref对象在组件整个生命周期一直存在,直到组件实例被销毁。

const Test = ()=> {
  const ref = React.useRef();
  return (
    <div
      onClick={() => {
        console.log(ref.current);
      }}
    >
     <input ref={ref} />
    </div>
  );
}

三、使用Refs的场景

使用Refs的场景有很多。最常见的场景是,访问目标元素。目标元素可以是DOM元素,或者组件实例。

除此之外,在某些情况下,我们可能需要在父组件中,访问子组件内部的目标元素。但ref属性已经用于访问子组件,没有办法传入子组件内部,访问目标元素。这种情况需要用的forwardRef api。

另外,Refs做为组件整个生命周一直存在的普通对象,设置ref.current不会触发组件的重新渲染。我们可以借用ref对象存储一些变量,用于实时访问。

1、引用 DOM 元素

把创建的ref对象,赋值给DOM元素的ref属性,可以实现对DOM元素的引用。

下面例子中,我们把testRef赋值给了input的ref属性。这样我们就可以通过testRef.current访问到input元素。然后执行input的focus()方法,使input获取焦点。

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

  action() {
    this.testRef.current.focus();
  }

  render() {
    return (
      <div>
        <input defaultValue="abc" ref={this.testRef} />
        <button onClick={() => this.action()}>引用input</button>
      </div>
    );
  }
}

2、引用组件实例

除了可以引用指定的dom元素,ref还可以引用组件实例。

下面例子中,我们把testRef赋值给Input组件的ref属性。同样,我们可以通过testRef.current,访问到Input组件的实例。

class Input extends React.Component {
  constructor(props) {
    super(props)
    this.state = { name: 'abc' }
  }

  render() {
    return (
      <input
        value={this.state?.name}
        onChange={(e) => this.setState({ name: e.target.value })}
      />
    )
  }
}

const Test = () => {
  const testRef = React.useRef(null)

  const log = () => {
    console.log(testRef?.current?.state.name)
  }

  return (
    <div>
      <Input ref={testRef} />
      <button onClick={log}>访问Input</button>
    </div>
  )
}

有一点需要注意,因为函数组件没有实例,所以ref不能引用函数组件。

3、Refs 转发

除了访问DOM元素 和 组件实例,有时候我们还需要访问组件内部的元素。比如,访问Button组件内部的button元素,访问Input组件内部的input元素。

令人遗憾的是,我们无法直接通过ref属性把ref对象传入组件内部。因为,ref属性会被特殊处理掉,用于访问组件实例本身。

当然,我们可以换一个props属性,把ref对象传入组件内部,然后访问目标元素。

比如:

const Text = ({ children, testRef }) => <div ref={testRef}>{children}</div>;

const Test = () => {
  const ref = React.useRef();

  useEffect(() => {
    console.log(ref.current);
  }, []);

  return (
    <div>
      <Text testRef={ref}>hello</Text>
    </div>
  );
}

【前端小课堂】—— React中Refs

a、使用forwardRef转发

实际上React官方为了解决此类问题,引入了forwardRef。forwardRef接受一个render函数做为参数,并返回一个组件。这个组件可以接受ref属性,并通过函数组件的第二个参数,转发到组件内部。

下面例子中,Text组件,经过forwordRef处理,所以ref属性不会像普通组件那样被特殊处理掉。而是作为render函数的第二个参数,传入到了组件内部,用于访问组件内部的div。

const Text = forwardRef({children}, ref) => <div ref={ref}>{children}</div>;

const Test = () => {
  const ref = React.useRef();

  useEffect(() => {
    console.log(ref.current);
  }, []);

  return (
      <Text ref={ref}>hello</Text>
  );
}

需要注意,普通函数组件,和class组件的构造函数,没有ref参数。

b、在高阶组件中转发

高阶组件(HOC)会透传(pass through)所有props到其包裹的组件,但是不会透传ref,ref同样会被特殊处理掉。因此下面例子中,ref对象绑定的是Log组件,而非TestButton。

function log(Cmp) {  
  class Log extends React.Component {
      componentDidUpdate(prevProps) {
        console.log(prevProps);
      }

      render() {
        return <Cmp {...this.props} />;    
      }
  }

  return Log;
}

class TestButton extends React.Component {
  focus() {
    // ...
  }
}

export default log(TestButton)
import TestButton from './TestButton'

const Test = () => {
  const ref = React.useRef();

  useEffect(() => {
    console.log(ref.current);
  }, []);

  return (
      <TestButton ref={ref}></TestButton>
  );
}

【前端小课堂】—— React中Refs

为了解决上面这个问题,同样需要借用forwardRef把ref转发到HOC内部被包裹的组件。

下面我们调整一下,高阶组件log的实现。实际上,这里用到了前面刚才讲过的知识。

  • 声明一个render函数,借用forwardRef()生成新的组件,用于接受和传递ref对象;
  • 换一个props名称(forwardedRef)把接收到的ref对象传入高阶组件内部;
  • 把ref对象,赋值给内部组件的ref属性,从而访问到内部组件
function log(Cmp) {
  class Log extends React.Component {
    componentDidUpdate(prevProps) {
      console.log(prevProps);
    }

    render() {
      let { forwardedRef, ...rest } = this.props;
      return <Cmp {...rest} ref={forwardedRef} />;
    }
  }

  const forward = (props, ref) => {
    return <Log {...props} forwardedRef={ref} />;
  };

  return React.forwardRef(forward);
}

c、使用useImperativeHandle定制暴露属性

首先,我们要时刻谨记,ref是破坏性的,应当尽量避免使用。即使极少数情况,不得不使用ref,也应当尽量减少组件对外暴露属性。

而且还有一个问题需要解决。ref不能直接访问函数组件,更无法使用函数组件内部定义的方法和state。

于是,React引入useImperativeHandle。useImperativeHandle是hooks,只能在函数组件中使用,而且需要结合forwordRef一起使用。

const Input = (props, ref) => {
  const inputRef = useRef();

  useImperativeHandle(ref, () => ({
    getVal: () => {
      return inputRef.current.value;
    },
  }));

  return <input defaultValue="test" ref={inputRef} />;
};

export default forwardRef(Input);
import Input from './input'

const Test = () {
  const ref = useRef();

  useEffect(() => {
    console.log(ref.current);
  }, []);

  return (
      <Input ref={ref} />
  );
}

只暴露了,getVal()

【前端小课堂】—— React中Refs

当然,你也可以通过相同的思路,暴露函数组件内部的属性或者方法。

const Test = (props, ref) => {
  const [vs, setVs] = useState(false);

  useImperativeHandle(ref, () => ({
    getVisible: () => {
      return vs;
    },
    setVisible: (vs) =>{
      setVs(vs)
    }
  }));

  return <SomeElement visible={vs} />;
}

export default forwardRef(Test)

【前端小课堂】—— React中Refs

4、函数组件中存储数据

函数组件中,useRef()创建的ref对象,能够在函数组件保持用一个引用对象。而且修改ref对象的current,不会触发组件的重新渲染。基于以上特性,可以在ref对象中存储一些数据。

a、存储稳定数据

有时候,我们需要在函数组件中存储一些稳定的数据。比如,setInterval()的id,以便后面清除setInterval。

const Test = () => {
    const timerID = useRef();

    const action = useCallback(() => {
        timerID.current = setInterval(()=>{
            // ...
        }, 1000); 
    },[]);

    useEffect(()=>{
        return () => clearInterval(timerID.current);
    },[]);
  
    return <button onClick={action}>执行</button>
}

b、存储previous value

ref对象,始终保持引用对象;而且dom render早于useEffect执行。

const Previous = () => {
  const [count, setCount] = useState(0);
  const preRef = useRef();

  useEffect(() => {
    preRef.current = count;
  }, [count]);

  return (
    <div>
      <section>previous: {preRef.current}</section>
      <section>current: {count}</section>
      <footer>
        <button onClick={() => setCount(count + 1)}>add 1</button>
      </footer>
    </div>
  );
}

可以简单封装usePrevious hooks

const usePrevious = (val) => {
  const [cur, setCur] = useState(val);
  const preRef = useRef();

  useEffect(() => {
    preRef.current = cur;
  }, [cur]);

  return [preRef.current, cur, setCur];
};

const Previous = () => {
  const [pre, cur, setCur] = usePrevious(0);

  return (
    <div>
      <section>previous: {pre}</section>
      <section>current: {cur}</section>
      <footer>
        <button onClick={() => setCur(cur + 1) }>add 1</button>
      </footer>
    </div>
  );
};

原则上,react并不提倡使用Refs。Refs被定义为逃生舱api。所谓逃生舱,只有在没有办法的情况下才会采用的方法。

转载自:https://juejin.cn/post/7213328766475059261
评论
请登录