5

引言

相信大部分同学对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>;
  }
}

打印结果

image.png

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

函数类型Ref

class App extends React.Component {
  dom = null
  children = null
  componentDidMount() {
    console.log(this.dom, this.childrenDom);
  }
  render() {
    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>;
  }
}

打印结果

image.png

对象类型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>
    </>
  );
}

一起看下打印结果

image.png
image.png

发现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给组件是无效的。

image.png
那如何传递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} />;
}

打印信息如下

image.png

通过别名的方式可以传递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的消息啦~~"} />;
}

页面展示情况
image.png

控制台打印结果

image.png

上面的代码就是通过别名配合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的消息啦~~"} />;
}

控制台打印结果

image.png

尽管我们传递下去的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属性

控制台打印结果

image.png

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存储的值在发生变化后是不会引起整个组件的重新渲染的。


Tqing
112 声望16 粉丝