引言

置信大部分同学对ref的认知还处于获取DOM节点和组件实例层面上,实际上除了这个性能,还有其它小技巧能够应用,这篇文章将具体地介绍ref的创立和应用,相干代码会以函数组件为主。

创立Ref

字符串类型Ref

class App extends React.Component {  render() {    console.log("this", this);    return (      <>        <div ref="dom">hello world</div>        <Children ref="children" />      </>    );  }}class Children extends React.Component {  render() {    return <div>china</div>;  }}

打印后果

无论是对于实在DOM,还是类组件,通过字符串创立的ref会绑定在this.refs对象上。

函数类型Ref

class App extends React.Component {  dom = null  children = null  render() {    componentDidMount() {      console.log(this.dom, this.childrenDom);    }    return (      <>        <div ref={(node) => this.dom = node}>hello world</div>        <Children ref={(node) => this.childrenDom = node} />      </>    );  }}class Children extends React.Component {  render() {    return <div>china</div>;  }}

打印后果

对象类型Ref

createRef创立Ref

创建对象类型的Ref对于类组件和函数组件的创立形式是不一样的,在类组件中创立Ref对象须要通过createRef函数来进行创立,这个函数的实现其实非常简单

function createRef() {    return {    current: null    }}

从下面的代码来看,就是创立并返回带有current属性的对象。

留神:不要在函数组件外面应用createRef函数

举个例子来看下为什么在函数组件外面不能应用createRef函数来创立Ref

function Counter() {  const ref1 = React.useRef(0);  const ref2 = React.createRef();  const [count, setCount] = React.useState(0);  console.log(ref1, ref2);  return (    <>      <div class="counter-area">        <div class="count1">count1: {ref1.current}</div>        <div class="count2">count2: {ref2.current}</div>      </div>      <button        onClick={() => {          ref1.current = (ref1.current || 0) + 1;          ref2.current = (ref2.current || 0) + 1;          setCount(count + 1);        }}        >        点我++      </button>    </>  );}

一起看下打印后果


发现count2的右侧始终没有打印,依据控制台外面打印的数据不难发现由createRef创立的refcurrent属性始终都为null,所以count2右侧没有数据。

函数组件更新UI视图实际上会将整个函数从新执行一遍,那么createRef函数在组件每次渲染的时候也会从新调用,生成初始状态的ref,对应的current属性为null

useRef创立Ref

React提供了内置的useRef来生成ref对象。

function Counter() {  const ref = React.useRef(null);  React.useEffect(() => {    console.log(ref.current);  }, []);  return <div ref={ref}>hello world</div>;}

下面有说过在函数组件外面通过createRef创立ref会有问题,那通过useRef这个函数生成的ref会不会有上述的问题?
答案是不会,在类组件外面能够把ref存储到实例对象下来,然而函数组件并没有实例的说法,然而函数组件有对应的Fiber对象,只有组件没有销毁,那么fiber对象也不会销毁,将useRef产生的ref挂载到对应的fiber对象上,那么ref就不会在函数每次从新执行时被重置掉。

Ref的高阶用法

Ref转发

咱们在通过Props的形式向组件传递信息时,某些特定的属性是会被React底层解决的,咱们在组件外面是无奈承受到的,例如keyref

function Counter(props) {  const { key, ref } = props;  console.log("props", props);  return (    <>      <div class="key">key: {key}</div>      <div class="ref">ref: {ref.current}</div>    </>  );}function App() {  const ref = useRef(null);  return <Counter key={"hello"} ref={ref} />;}

控制台中打印信息如下,能够理解到通过props的形式传递keyref给组件是有效的。


那如何传递refkey给子组件,既然react不容许,那么咱们换个身份,暗度陈仓。

function Counter(props) {  const { pkey, pref } = props;  console.log("props", props);  return (    <>      <div class="key">key: {pkey}</div>      <div class="ref">ref: {pref.current}</div>    </>  );}function App() {  const ref = useRef(null);  return <Counter pkey={"hello"} pref={ref} />;}

打印信息如下

通过别名的形式能够传递ref,那么为什么还须要forwardRef来传递Ref?

假如这样一种场景,你的组件中须要援用内部库提供的组件,例如antdfusion这种组件库提供的组件,而且你须要传递ref给这个组件,因为援用的内部组件是不可能晓得你会用什么样的别名属性来传递ref,所以这个时候只能应用forwardRef来进行传递ref,因为这个是react规定用来传递ref值的属性名。

跨层级传递Ref

场景:须要把refGrandFather组件传递到Son组件,在Son组件外面展现ref存储的信息并获取Son组件外面的dom节点。

const Son = (props) => {  const { msg, grandFatherRef } = props;  return (    <>      <div class="msg">msg: {msg}</div>      <div class="ref" ref={grandFatherRef}>        ref: {grandFatherRef.current}      </div>    </>  );};const Father = forwardRef((props, ref) => {  return <Son grandFatherRef={ref} {...props} />;});function GrandFather() {  const ref = useRef("我是来自GrandFather的ref啦~~");  useEffect(() => console.log(ref.current), []);  return <Father ref={ref} msg={"我是来自GrandFather的音讯啦~~"} />;}

页面展现状况

控制台打印后果

下面的代码就是通过别名配合forwardRef来转发ref,这种转发ref的形式,是十分常见的用法。通过别名的形式传递ref和通过forwardRef传递ref的形式其实没有太大的差异,最实质的区别就是通过别名的形式须要传递方和接管方人为地都约定好属性名,而通过forwardRef的形式是react外面约定了传递的ref属性名。

合并转发Ref

咱们从父组件传递上来的ref不仅仅能够用来展现获取某个dom节点,还能够在子组件外面更改ref的信息,获取子组件外面其它信息。
场景:咱们须要获取子组件外面input和button这两个dom节点

const Son = forwardRef((props, ref) => {  console.log("props", props);  const sonRef = useRef({});  useEffect(() => {    ref.current = {      ...sonRef.current,    };    return () => ref.current = {}  }, []);  return (    <>      <button ref={(button) => Object.assign(sonRef.current, { button })}>        点我      </button>      <input        type="text"        ref={(input) => Object.assign(sonRef.current, { input })}        />    </>  );});function Father() {  const ref = useRef("我是来自Father的ref啦~~");  useEffect(() => console.log(ref.current), []);  return <Son ref={ref} msg={"我是来自Father的音讯啦~~"} />;}

控制台打印后果

只管咱们传递上来的refcurrent为字符串属性,然而咱们通过在子组件外面批改current属性,进而获取到了子组件外面buttoninputdom节点

下面的代码其实能够把useEffect更改成为**useImperativeHandle**,这是react提供的hook,用来配合forwardRef来进行应用,能够。

**useImperativeHandle**接管三个参数:

  1. 通过forwardRef传递过去的ref
  2. 处理函数,其返回值裸露给父组件的ref对象
  3. 依赖项

    const Son = forwardRef((props, ref) => {  console.log("props", props);  const sonRef = useRef({});  useImperativeHandle( ref, () => ({   ...sonRef.current, }), []  );  return ( <>   <button ref={(button) => Object.assign(sonRef.current, { button })}>     点我   </button>   <input     type="text"     ref={(input) => Object.assign(sonRef.current, { input })}     /> </>  );});function Father() {  const ref = useRef("我是来自Father的ref啦~~");  useEffect(() => console.log(ref.current), []);  return <Son ref={ref} msg={"我是来自Father的音讯啦~~"} />;}

    高阶组件转发Ref

    在应用高阶组件包裹一个原始组件的时候,因为高阶组件会返回一个新的组件,如果不进行ref转发,从下层组件传递下来的ref会指向这个新的组件

    function HOC(Comp) {  function Wrapper(props) { const { forwardedRef, ...restProps } = props; return <Comp ref={forwardedRef} {...restProps} />;  }  return forwardRef((props, ref) => ( <Wrapper forwardedRef={ref} {...props} />  ));}const Son = forwardRef((props, ref) => {  return <div ref={ref}>hello world</div>;});const NewSon = HOC(Son);function Father() {  const ref = useRef("我是来自Father的ref啦~~");  useEffect(() => console.log(ref.current), []);  return <NewSon ref={ref} msg={"我是来自Father的音讯啦~~"} />;}
    留神:第12行处减少forwardRef转发是因为函数组件无奈间接接管ref属性

控制台打印后果

Ref实现组件间的通信

子组件能够通过props获取和扭转父组件外面的state,父组件也能够通过ref来获取和扭转子组件外面的state,实现了父子组件之间的双向通信,这其实在肯定水平上突破了react单向数据传递的准则。

const Son = forwardRef((props, ref) => {  const { toFather } = props;  const [fatherMsg, setFatherMsg] = useState('');  const [sonMsg, setSonMsg] = useState('');  useImperativeHandle(    ref,    () => ({      fatherSay: setFatherMsg    }),    []  );  return (    <div className="son-box">      <div className="father-say">父组件对我说: {fatherMsg}</div>      <div className="son-say">        我对父组件说:        <input          type="text"          value={sonMsg}          onChange={(e) => setSonMsg(e.target.value)}        />        <button onClick={() => toFather(sonMsg)}>to father</button>      </div>    </div>  );});function Father() {  const [fatherMsg, setFatherMsg] = useState('');  const [sonMsg, setSonMsg] = useState('');  const sonRef = useRef(null);  return (    <div className="father-box">      <div className="son-say">子组件对我说: {sonMsg}</div>      <div className="father-say">        对子组件说:        <input          type="text"          value={fatherMsg}          onChange={(e) => setFatherMsg(e.target.value)}        />        <button onClick={() => sonRef.current.fatherSay(fatherMsg)}>          to son        </button>      </div>      <Son ref={sonRef} toFather={setSonMsg} />    </div>  );}

Ref和State的抉择

咱们在平时的学习和工作中,在应用函数组件的时候,经常会应用useState来生成state,每次state扭转都会引发整个函数组件从新渲染,然而有些state的扭转不须要更新视图,那么咱们能够思考应用ref来存储,ref存储的值在发生变化后是不会引起整个组件的从新渲染的。