1、什么是Refs?

官网是这么说的:

Refs 提供了一种形式,容许咱们拜访 DOM 节点或在 render 办法中创立的 React 元素。

在react数据流中,props是父子组件交换的惟一形式。如果要批改一个子组件的状态,那么就须要应用新的props去从新渲染。然而,在某些状况下,咱们须要在props数据流之外,强制去批改子组件。被批改的子组件可能是一个React组件的实例,也可能是DOM元素。React官网对于这两种状况都给出了解决办法。

2、类组件中应用ref

React.createRef()

这种形式是应用 React.createRef() 创立的ref,并通过 ref 属性附加到 React 元素。在结构组件时,通常将 Refs 调配给实例属性,以便能够在整个组件中援用它们。

当 ref 被传递给 render 中的元素时,对该节点的援用能够在 ref 的 current 属性中被拜访。

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

  • 当 ref 属性用于 HTML 元素时,构造函数中应用 React.createRef() 创立的 ref 接管底层 DOM 元素作为其 current 属性。
  • 当 ref 属性用于自定义 class 组件时,ref 对象接管组件的挂载实例作为其 current 属性。
  • 你不能在函数组件上应用 ref 属性,因为他们没有实例。
export class App extends React.Component {  constructor(props) {    super(props);    this.myDOM = React.createRef();    this.myComponent = React.createRef();    this.myFunction = React.createRef();  }  log() {    console.log('myDOM', this.myDOM);    console.log('myComponent', this.myComponent);    console.log('myFunction', this.myFunction);  }  render() {    return (      <div>        <button onClick={() => this.log()}> click</button>        <div ref={this.myDOM}>DOM</div>        <MyComponent ref={this.myComponent} />        <MyFunction ref={this.myFunction} />      </div>    )  }}class MyComponent extends React.Component {  constructor(props) {    super(props);  }  render() {    return <div>MyComponent</div>;  }}function MyFunction(params) {  return (    <div>MyFunction</div>  )}

callback Ref

export class App extends React.Component {  constructor(props) {    super(props);    this.myDOM = null    this.myComponent = null    this.myFunction = null  }  log() {    console.log('myDOM', this.myDOM);    console.log('myComponent', this.myComponent);    console.log('myFunction', this.myFunction);  }  render() {    return (      <div>        <button onClick={() => this.log()}> click</button>        <div ref={ele => this.myDOM = ele}>DOM</div>        <MyComponent ref={ele => this.myComponent = ele} />        <MyFunction ref={ele => this.myFunction = ele} />      </div>    )  }}class MyComponent extends React.Component {  constructor(props) {    super(props);  }  render() {    return <div>MyComponent</div>;  }}function MyFunction(params) {  return (    <div>MyFunction</div>  )}

React 将在组件挂载时,会调用 ref 回调函数并传入 DOM 元素,当卸载时调用它并传入 null。在 componentDidMount 或 componentDidUpdate 触发前,React 会保障 refs 肯定是最新的。

如果要在函数组件中应用 ref,你能够应用 forwardRef(可与 useImperativeHandle 联合应用),或者能够将该组件转化为 class 组件。

不管怎样,你能够在函数组件外部应用 ref 属性,只有它指向一个 DOM 元素或 class 组件。

3、forwardRef

Ref 转发是一项将 ref 主动地通过组件传递到其一子组件的技巧

通过forwardRef能够获取函数组件中的DOM元素

export class App extends React.Component {  constructor(props) {    super(props);    this.myDOM = null    this.myComponent = null    this.myFunction = null  }  log() {    console.log('myDOM', this.myDOM);    console.log('myComponent', this.myComponent);    console.log('myFunction', this.myFunction);  }  render() {    return (      <div>        <button onClick={() => this.log()}> click</button>        <div ref={ele => this.myDOM = ele}>DOM</div>        <MyComponent ref={ele => this.myComponent = ele} />        <MyFunction ref={ele => this.myFunction = ele} />      </div>    )  }}class MyComponent extends React.Component {  constructor(props) {    super(props);  }  render() {    return <div>MyComponent</div>;  }}const MyFunction = React.forwardRef((props, ref) => {  return (    <div ref={ref}>MyFunction</div>  )})

4、useRef的应用

const refContainer = useRef(initialValue);

useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内放弃不变。

export function App() {  return <TextInputWithFocusButton />}function TextInputWithFocusButton() {  const inputEl = useRef(null);  const onButtonClick = () => {    inputEl.current.focus();  };  return (    <>      <input ref={inputEl} type="text" />      <button onClick={onButtonClick}>Focus the input</button>    </>  );}

useRef不仅能够保留DOM元素和class组件实例,也能够用来保留任何值。

export function App() {  return <TextInputWithFocusButton />}function TextInputWithFocusButton() {  const inputEl = useRef(90);  const onButtonClick = () => {    console.log(inputEl);  };  return (    <>      <button onClick={() => inputEl.current += 1}>+</button>      <button onClick={() => inputEl.current -= 1}>-</button>      <button onClick={onButtonClick}>Focus the input</button>      <div>{inputEl.current}</div>    </>  );}

inputEl的current属性值为90,点击"+" 按钮将会在inputEl.current的根底上+1,扭转valRef.current不会触发组件的更新

因而咱们须要别的形式来在ref.current变更时,触发组件更新,展现最新的ref.current值,应用useState强制更新。

export function App() {  return <TextInputWithFocusButton />}function TextInputWithFocusButton() {  const inputEl = useRef(90);  const [state, setState] = useState({})  const onButtonClick = () => {    console.log(inputEl);  };  return (    <>      <button onClick={() => {        inputEl.current += 1;        setState({})      }}>+</button>      <button onClick={() => {        inputEl.current -= 1;        setState({})      }}>-</button>      <button onClick={onButtonClick}>Focus the input</button>      <div>{inputEl.current}</div>    </>  );}

React.createRef能够用于class组件和Function组件,React.useRef必须用于函数组件。当React.createRef()用于Fucntion组件的时候,可能呈现一些问题。

export function App() {  return <TextInputWithFocusButton />}function TextInputWithFocusButton() {  const inputEl = React.createRef();  const [state, setState] = useState({});  useEffect(() => {    inputEl.current = 100;  }, [])  const onButtonClick = () => {    console.log(inputEl);  };  return (    <>      <button onClick={() => {        inputEl.current += 1;        setState({})      }}>+</button>      <button onClick={() => {        inputEl.current -= 1;        setState({})      }}>-</button>      <button onClick={onButtonClick}>Focus the input</button>      <div>{inputEl.current}</div>    </>  );}

咱们用createRef替换掉useRef,在这里无论点击多少次按钮,最初还是会显示为空(值为null):

这是因为函数组件的行为依然是函数,他们在渲染和重渲染的 就像一般的函数一样

所以,当App这个函数组件被从新渲染时,App函数将会执行,并且从新创立、初始化所有的变量和表达式。因而,createRef每次都会被执行,所以对应的值总是为null。

5、useRef解决了什么问题

为了在函数组件中保留状态,useRef就被发明进去,它将会在函数组件的生命周期中,放弃状态不变,除非手动进行批改。

函数式组件中,咱们应该应用useRef去创立咱们的ref对象,useRef 总会在每次渲染时返回同一个 ref 对象,除非手动扭转。

然而,当 ref 对象内容发生变化时,useRef 并不会告诉你。变更 .current 属性不会引发组件从新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则须要应用回调 ref 来实现。

export function App() {  return <MeasureExample />}function MeasureExample() {  const [height, setHeight] = useState(0);  const measureRef = useCallback(node => {    if (node !== null) {      setHeight(node.getBoundingClientRect().height);    }  }, []);  return (    <>      <Child measureRef={measureRef} />      {height > 0 &&        <h2>The above header is {Math.round(height)}px tall</h2>      }    </>  );}function Child({ measureRef }) {  const [show, setShow] = useState(false);  if (!show) {    return (      <button onClick={() => setShow(true)}>        Show child      </button>    );  }  return (    <>      <button onClick={() => setShow(false)}>        Show child      </button>      <h1 ref={measureRef}>Hello, world</h1>    </>  )}

在这里,咱们没有抉择应用useRef,而是应用了Callback Ref能够确保 即使子组件提早显示被测量的节点(比方为了响应一次点击),咱们仍然可能在父组件接管到相干的信息,以便更新测量后果。

在此示例中,当且仅当组件挂载和卸载时,callback ref 才会被调用,因为渲染的 <h1> 组件在整个从新渲染期间始终存在。如果你心愿在每次组件调整大小时都收到告诉,则可能须要应用 ResizeObserver 或基于其构建的第三方 Hook。

6、useImperativeHandle

useImperativeHandle(ref, createHandle, [deps])

useImperativeHandle能够让你在应用ref时自定义裸露给父组件的实例。不要间接应用forward Ref,forward Ref应该与useImperativeHandle一起应用

  • ref:定义 current 对象的 ref createHandle:一个函数,返回值是一个对象,即这个 ref 的 current
  • 对象 [deps]:即依赖列表,当监听的依赖发生变化,useImperativeHandle 才会从新将子组件的实例属性输入到父组件
  • ref 的 current 属性上,如果为空数组,则不会从新输入。

它解决了什么问题

下面咱们讲述了forward Ref的一些问题,forward Ref会将子组件的实例间接裸露给父组件,父组件能够对子组件实例做任意的操作,那么,useImperativeHandle就是来解决这个问题的,useImperativeHandle能够让咱们抉择裸露一些咱们须要被非凡解决的操作。

  • 通过useImperativeHandle的Hook, 将父组件传入的ref和useImperativeHandle第二个参数返回的对象绑定到了一起
  • 所以在父组件中, 调用inputRef.current时, 实际上是返回的对象
  • 缩小裸露给父组件获取的DOM元素属性, 只裸露给父组件须要用到的DOM办法

间接应用forward Ref

export function App() {  return <Example />}// 实现 ref 的转发const FancyButton = React.forwardRef((props, ref) => (  <div>    <input ref={ref} type="text" />    <button>{props.children}</button>  </div>));// 父组件中应用子组件的 reffunction Example() {  const ref = useRef();  const handleClick = useCallback(() => ref.current.focus(), [ ref ]);  return (    <div>      <FancyButton ref={ref}>Click Me</FancyButton>      <button onClick={handleClick}>获取焦点</button>    </div>  )}

配合useImperativeHandle一起应用

export function App() {  return <Example />}const FancyInput = React.forwardRef((props, ref) => {  const inputRef = useRef();  useImperativeHandle(ref, () => ({    focus: () => {      inputRef.current.focus();    }  }));  return <input ref={inputRef} type="text" />});const Example = () => {  const fancyInputRef = useRef();  return (    <div>      <FancyInput ref={fancyInputRef} />      <button        onClick={() => fancyInputRef.current.focus()}      >父组件调用子组件的 focus</button>    </div>  )}

下面这个例子中与间接转发 ref 不同,间接转发 ref 是将 React.forwardRef 中函数上的 ref 参数间接利用在了返回元素的 ref 属性上,其实父、子组件援用的是同一个 ref 的 current 对象,官网不倡议应用这样的 ref 透传,而应用 useImperativeHandle 后,能够让父、子组件别离有本人的 ref,通过 React.forwardRef 将父组件的 ref 透传过来,通过 useImperativeHandle 办法来自定义凋谢给父组件的 current。

useImperativeHandle 的第一个参数是定义 current 对象的 ref,第二个参数是一个函数,返回值是一个对象,即这个 ref 的 current 对象,这样能够像下面的案例一样,通过自定义父组件的 ref 来应用子组件 ref 的某些办法

export function App() {  return <Example />}const FancyInput = React.forwardRef((props, ref) => {  const [fresh, setFresh] = useState(false)  const attRef = useRef(0);  useImperativeHandle(ref, () => ({    attRef,    fresh  }), [fresh]);  const handleClick = useCallback(() => {    attRef.current++;  }, []);  return (    <div>      {attRef.current}      <button onClick={handleClick}>Fancy</button>      <button onClick={() => setFresh(!fresh)}>刷新</button>    </div>  )});const Example = () => {  const fancyInputRef = useRef();  return (    <div>      <FancyInput ref={fancyInputRef} />      <button        onClick={() => console.log(fancyInputRef.current)}      >父组件拜访子组件的实例属性</button>    </div>  )}