什么是Ref

Ref意味着reference,所以ref可以指定任何(DOM node, JavaScript value, …)

基本使用

类组件中通过React.createRef()来创建一个ref,并且在dom或者组件中传递ref的props
函数式组件中通过useRef来创建ref

ref 的值根据节点的类型而有所不同:

  • 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性。
  • 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上使用 **ref** 属性,因为他们没有实例。
  • createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用
  1. //类组件
  2. class MyComponent extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. this.myRef = React.createRef();
  6. }
  7. render() {
  8. return <div ref={this.myRef} />;
  9. }
  10. }
  11. **********************************************
  12. //函数式组件
  13. const MyComponent = () => {
  14. myRef = React.useRef();
  15. return <div ref={myRef} />;
  16. }

React 会在组件挂载时给 current 属性传入 DOM 元素,并在组件卸载时传入 null 值。ref 会在 componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新。

Instance Variable

used to create a mutable object. The mutable object’s properties can be changed at will without affecting the Component’s state.

在组件中,Ref可以用作保存Instance Variable。
例如,我们控制组件初次挂载和更新的时候分别展示不同渲染内容:

  1. import * as React from "react";
  2. const App = () => {
  3. const [count, setCount] = React.useState(0);
  4. function onClick() {
  5. setCount(count + 1);
  6. }
  7. const isFirstRender = React.useRef(true);
  8. React.useEffect(() => {
  9. if (isFirstRender.current) {
  10. isFirstRender.current = false;
  11. } else {
  12. console.log(
  13. `
  14. I am a useEffect hook's logic
  15. which runs for a component's
  16. re-render.
  17. `
  18. );
  19. }
  20. });
  21. return (
  22. <div>
  23. <p>{count}</p>
  24. <button type="button" onClick={onClick}>
  25. Increase
  26. </button>
  27. {/*
  28. Only works because setCount triggers a re-render.
  29. Just changing the ref's current value doesn't trigger a re-render.
  30. */}
  31. <p>{isFirstRender.current ? "First render." : "Re-render."}</p>
  32. </div>
  33. );
  34. };
  35. export default App;
  • **useRef** 在渲染周期内永远不会变,因此可以用来引用某些数据。
  • 修改 **ref.current** 不会引发组件重新渲染。

Rule of thumb: Whenever you need to track state in your React component which shouldn’t trigger a re-render of your component, you can use React’s useRef Hooks to create an instance variable for it.

更多详细使用参见 useEffect-ONLY ON UPDATE

Dom Refs

很多场景下,我们需要和HTML元素进行交互,需要访问DOM,

下面我们通过一个示例来说明如何获取dom

  1. import * as React from "react";
  2. const App = () => {
  3. const ref = React.useRef(); //(1)创建ref
  4. const [text, setText] = React.useState("Some text ...");
  5. function handleOnChange(event) {
  6. setText(event.target.value);
  7. }
  8. React.useEffect(() => {
  9. //(3)访问Ref
  10. const { width } = ref.current.getBoundingClientRect();
  11. console.log(`Width:${width}`);
  12. },[text]);
  13. return (
  14. <div>
  15. <input type="text" value={text} onChange={handleOnChange} />
  16. <div>
  17. // (2)给HTML元素提供一个ref的HTML attribute
  18. <span ref={ref}>{text}</span>
  19. </div>
  20. </div>
  21. );
  22. };
  23. export default App;

我们来看下获取dom的ref使用方法,(1) (2) (3)

  1. 通过React.useRef创建一个ref object
  2. 给HTML元素提供一个ref的HTML attribute,React会自动为的给dom节点赋予ref特性
  3. 最后我们可以通过ref.current来获取dom节点

每次改变输入框的输入值也就是text状态发生变化的时候 我们都可以访问到输入框span的宽度。

Callback Refs

React 也支持另一种设置 refs 的方式,称为“回调 refs”。它能助你更精细地控制何时 refs 被设置和解除。

不同于传递 createRef() 创建的 ref 属性,你会传递一个函数。这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。

在dom节点上添加ref属性,通过ref = (node)=> {} 来获取dom
image.png

image.png
https://stackblitz.com/edit/react-tvvq5r

  1. import React from "react";
  2. import "./style.css";
  3. export default function App() {
  4. const [text, setText] = React.useState("Some text ...");
  5. function handleOnChange(event) {
  6. setText(event.target.value);
  7. }
  8. const ref = React.useCallback(
  9. node => {
  10. console.log(node);
  11. if (!node) return;
  12. const { width } = node.getBoundingClientRect();
  13. if (width >= 100) {
  14. node.style.color = "red";
  15. } else {
  16. node.style.color = "blue";
  17. }
  18. console.log(`Width:${width}`);
  19. },
  20. [text]
  21. );
  22. return (
  23. <div>
  24. <input type="text" value={text} onChange={handleOnChange} />
  25. <div>
  26. <span ref={ref}>{text}</span>
  27. </div>
  28. </div>
  29. );
  30. }

不需要创建ref,更不需要ref.current来访问ref
通过回调函数,直接可以获取到dom。

Refs转发-forwardRef

Ref 转发是一项将 ref 自动地通过组件传递到其子组件的技巧。(说白了就是父组件获取子组件的一种方式)
Ref 转发是一个可选特性,其允许某些组件接收 **ref**,并将其向下传递(换句话说,“转发”它)给子组件。

我们先看下类组件中,如何获取子组件的ref:

  1. class Counter extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.state = {
  5. count: 0,
  6. };
  7. this.AddButtonRef = React.createRef();
  8. }
  9. handleAdd = () => {
  10. const { count } = this.state;
  11. this.setState({ count: count + 1 });
  12. };
  13. render() {
  14. return (
  15. <>
  16. Counter子组件count:{this.state.count}
  17. <button onClick={this.handleAdd} ref={this.AddButtonRef}>
  18. 点击增加
  19. </button>
  20. </>
  21. );
  22. }
  23. }
  24. class App extends React.Component {
  25. constructor(props) {
  26. super(props);
  27. this.CountRef = React.createRef();
  28. }
  29. handleRef = () => {
  30. console.log(this.CountRef);
  31. const buttonText = this.CountRef.current.AddButtonRef.current.innerHTML;
  32. console.log(buttonText);
  33. // this.CountRef.current.handleAdd();
  34. };
  35. render() {
  36. return (
  37. <>
  38. <div>App</div>
  39. <button onClick={this.handleRef}>点击获取子组件内容</button>
  40. <Counter ref={this.CountRef} />
  41. </>
  42. );
  43. }
  44. }
  45. export default App;

image.png
Counter子组件中有button,父组件APP想要获取button的ref,通过在子组件Counter上定义createRef就可以获取相应的ref了,比较简单。

  1. class Demo extends React.Component {
  2. render() {
  3. return <>类组件</>;
  4. }
  5. }
  6. function Counter() {
  7. return <>函数组件</>;
  8. }
  9. const App = () => {
  10. const countRef = React.useRef();
  11. setTimeout(() => console.log(222, countRef), 2000);
  12. return (
  13. <div>
  14. <div>类组件Demo可以拿到ref,函数式组件Counter拿不到</div>
  15. <Demo />
  16. <Counter ref={countRef} />
  17. </div>
  18. );
  19. };

函数式组件没有this,通过forwardRef可以获取函数式组件的ref

在函数组件中要获取子组件的数据,需要两步骤

  1. 将ref传递到子组件中,
  2. 需要使用forwardRef对子组件进行包装 ```javascript //2. 子组件通过React.forwardRef 进行转发 const Counter = React.forwardRef(({ item }, ref) => { const [count, setCount] = useState(0);

    const handleAdd = () => { setCount(count + 1); };

    return (

    Counter子组件count:{count} {/* 3. 子组件向上暴露ref/}
    ); }); function App() { //1. 父组件定义ref const CountRef = useRef(); const handleRef = () => { const buttonText = CountRef.current.innerHTML; console.log(buttonText); }; return ( <>
    App
    </> ); }

export default App;

  1. AppCounter组件是父子组件关系,想在App父组件中访问子组件Counter中的li dom节点
  2. - 子组件被React.forwardRef(Component(props,ref))包裹,子组件第二个参数是ref,指向子组件的dom节点
  3. - 父组件创建ref,访问子组件ref
  4. > 注意
  5. > 第二个参数 `ref` 只在使用 `React.forwardRef` 定义组件时存在。常规函数和 class 组件不接收 `ref` 参数,且 props 中也不存在 `ref`
  6. > Ref 转发不仅限于 DOM 组件,你也可以转发 refs class 组件实例中。
  7. <a name="JuIjC"></a>
  8. # useRef和createRef区别
  9. - 两者都是获取 ref 的方式,都有一个 current 属性。
  10. - useRef 只能用于函数组件,createRef 可以用在类组件中。
  11. - useRef 在每次重新渲染后都保持不变,而 createRef 每次都会发生变化。?
  12. - **createRef 每次渲染都会返回一个新的引用,而 useRef 每次都会返回相同的引用**
  13. ```javascript
  14. import React, { useRef, useEffect, useState } from 'react';
  15. const Page1 = () => {
  16. const myRef2 = useRef(0);
  17. const [count, setCount] = useState(0)
  18. useEffect(()=>{
  19. myRef2.current = count;
  20. });
  21. function handleClick(){
  22. setTimeout(()=>{
  23. console.log(count); // 3
  24. console.log(myRef2.current); // 6
  25. },3000)
  26. }
  27. return (
  28. <div>
  29. <div onClick={()=> setCount(count+1)}>点击count</div>
  30. <div onClick={()=> handleClick()}>查看</div>
  31. </div>
  32. );
  33. }
  34. export default Page1;

类组件在更新的时候只会调用render、componentDidUpdate等生命周期
类组件只在组件初始化的时候创建ref,之后的更新过程都不会重新初始化class组件的实例,因此ref的值会在class组件的声明周期中保持不变(除非手动更新)。

useImperativeHandle

使用场景:通过 ref 获取到的是整个 dom 节点,通过 useImperativeHandle 可以控制只暴露一部分方法和属性,而不是整个 dom 节点。

useImperativeHandle可以让父组件获取并执行子组件内某些自定义函数(方法)。本质上其实是子组件将自己内部的函数(方法)通过useImperativeHandle添加到父组件中useRef定义的对象中

基本使用

useImperativeHandle(ref,create,[deps])函数前2个参数为必填项,第3个参数为可选项。

第1个参数为父组件通过useRef定义的引用变量;
第2个参数为子组件要附加给ref的对象,该对象中的属性即子组件想要暴露给父组件的函数(方法);
第3个参数为可选参数,为函数的依赖变量。凡是函数中使用到的数据变量都需要放入deps中,如果处理函数没有任何依赖变量,可以忽略第3个参数。

请注意:
1、这里面说的“勾住子组件内自定义函数”本质上是子组件将内部自定义的函数添加到父组件的ref.current上面。
2、父组件若想调用子组件暴露给自己的函数,可以通过 res.current.xxx 来访问或执行。

  1. const xxx = () => {
  2. //do smoting...
  3. }
  4. useImperativeHandle(ref,() => ({xxx}));

特别注意:() => ({xxx}) 不可以再简写成 () => {xxx},如果这样写会直接react报错。
因为这两种写法意思完全不一样:
1、() => ({xxx}) 表示 返回一个object对象,该对象为{xxx}
2、() => {xxx} 表示 执行 xxx 语句代码

  1. import React, { useState, useEffect, useRef, useImperativeHandle } from 'react';
  2. import './style.css';
  3. //2. 子组件通过React.forwardRef 进行转发
  4. const Counter = React.forwardRef(({ item }, ref) => {
  5. const [count, setCount] = useState(0);
  6. //3.将子组件的方法暴露给父组件
  7. useImperativeHandle(ref, () => ({
  8. handleAdd,
  9. }));
  10. const handleAdd = () => {
  11. setCount(count + 1);
  12. };
  13. return (
  14. <div>
  15. Counter子组件count:{count}
  16. <button onClick={handleAdd}>点击增加</button>
  17. </div>
  18. );
  19. });
  20. function App() {
  21. //1. 父组件定义ref
  22. const CountRef = useRef();
  23. const handleRef = () => {
  24. CountRef.current.handleAdd();
  25. console.log(CountRef.current);
  26. };
  27. return (
  28. <>
  29. <div>App</div>
  30. <button onClick={handleRef}>点击获取子组件内容</button>
  31. <Counter ref={CountRef} />
  32. </>
  33. );
  34. }
  35. export default App;

image.png

可以看到,父组件中获取到子组件中的handleAdd方法并执行了,count+1

拆解说明:

1、子组件内部先定义一个 xxx 函数
2、通过useImperativeHandle函数,将 xxx函数包装成一个对象,并将该对象添加到父组件内部定义的ref中。
3、若 xxx 函数中使用到了子组件内部定义的变量,则还需要将该变量作为 依赖变量 成为useImperativeHandle第3个参数,上面示例中则选择忽略了第3个参数。
4、若父组件需要调用子组件内的 xxx函数,则通过:res.current.xxx()。
5、请注意,该子组件在导出时必须被 React.forwardRef()包裹住才可以。

思考一下真的有必要使用useImperativeHandle吗?

从实际运行的结果,无论点击子组件还是父组件内的按钮,都将执行 addCount函数,使 count+1。
react为单向数据流,如果为了实现这个效果,我们完全可以把需求转化成另外一种说法,即:
1、父组件内定义一个变量count 和 addCount函数
2、父组件把 count 和 addCount 通过属性传值 传递给子组件
3、点击子组件内按钮时调用父组件内定义的 addCount函数,使 count +1。
你会发现即使把需求中的 父与子组件 描述对调一下,“最终实际效果”是一样的。
所以,到底使用哪种形式,需要根据组件实际需求来做定夺。

Reference

https://www.robinwieruch.de/react-ref
https://zh-hans.reactjs.org/docs/refs-and-the-dom.html
https://zh-hans.reactjs.org/docs/forwarding-refs.html
https://github.com/puxiao/react-hook-tutorial/blob/master/13%20useImperativeHandle%E5%9F%BA%E7%A1%80%E7%94%A8%E6%B3%95.md