众所周知,React 通过声明式的渲染机制把复杂的 DOM 操作抽象成为简单的 state 与 props 操作,一时圈粉无数,一夜间将前端工程师从面条式的 DOM 操作中拯救出来。尽管我们一再强调在 React 开发中尽量避免 DOM 操作,但在一些场景中仍然无法避免。当然 React 并没有把路堵死,它提供了 ref 用于访问在 render 方法中创建的 DOM 元素或者是 React 组件实例。

在 React v16.3 之前,ref 通过字符串(string ref)或者回调函数(callback ref)的形式进行获取,在 v16.3 中,经 0017-new-create-ref 提案引入了新的 React.createRef API。

image.png

在 React.createRef 出现之前,string ref 就已被诟病已久,React 官方文档直接提出 string ref 将会在未来版本被移出,建议用户使用 callback ref 来代替,为何需要这么做呢?主要原因集中于以下几点:

  • 当 ref 定义为 string 时,需要 React 追踪当前正在渲染的组件,在 reconciliation 阶段,React Element 创建和更新的过程中,ref 会被封装为一个闭包函数,等待 commit 阶段被执行,这会对 React 的性能产生一些影响。
  • 当使用 render callback 模式时,使用 string ref 会造成 ref 挂载位置产生歧义。
  • string ref 无法被组合,例如一个第三方库的父组件已经给子组件传递了 ref,那么我们就无法再在子组件上添加 ref 了,而 callback ref 可完美解决此问题。
  • 在根组件上使用无法生效。
  • 对于静态类型较不友好,当使用 string ref 时,必须显式声明 refs 的类型,无法完成自动推导。
  • 编译器无法将 string ref 与其 refs 上对应的属性进行混淆,而使用 callback ref,可被混淆。

详细参考:React ref 的前世今生

1、父组件操作子组件DOM

1.1、函数组件——React.forwardRef

React16 提供了一个关于 ref 的 API React.forwardRef,主要用于穿过父元素直接获取子元素的 ref,起到一个中转ref的作用,无需由我们自己来实现一个HOC

function forwardRef<T, P = {}>(render: ForwardRefRenderFunction<T, P>): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

interface ForwardRefRenderFunction<T, P = {}> {
    (props: PropsWithChildren<P>, ref: ForwardedRef<T>): ReactElement | null;
    displayName?: string;
    // explicit rejected with `never` required due to
    // https://github.com/microsoft/TypeScript/issues/36826
    /**
     * defaultProps are not supported on render functions
     */
    defaultProps?: never;
    /**
     * propTypes are not supported on render functions
     */
    propTypes?: never;
}

ref不能通过props传递,React.forwardRef的出现使之成为可能。React.forwardRef是一个HOC,接收一个函数组件,使这个函数组件接收ref。

这里需要注意,泛型类型T指ref的类型,P指props类型。

// 父组件
const Father = () => {
    const ref = React.createRef<HTMLInputElement>();
    
    useEffect(() => {
        if (ref.current) {
            ref.current.focus();
        }
    })
    
    return <Child ref={ref}></Child>
}

// 子组件
const Child = React.forwardRef<HTMLInputElement, any>((props, ref) => {
    return <input type="text" ref={ref}>child</input>
})
1.2、类组件——React.createRef
// 父组件
class Father extends React.Component<{}, {}> {

  private inpurRef = React.createRef<HTMLInputElement>();

  componentDidMount() {
    this.inpurRef.current?.focus();
  }

  render() {
    return <Child inputRef={this.inpurRef}></Child>
  }
}

export default Father;

// 子组件
interface IProps {
  inputRef: React.RefObject<HTMLInputElement>
}

class Child extends React.Component<IProps, {}> {

  render() {
    const { inputRef } = this.props;
    return <input type="text" ref={inputRef}/>
  }
}

export default Child;
1.3、类组件——callback ref

ref 通过回调函数(callback ref)的形式进行获取

// 父组件
class Father extends React.Component<{}, {}> {
  private inputRef: HTMLInputElement | null = null;

  generateRef = (el: HTMLInputElement) => {
    this.inputRef = el;
  }

  componentDidMount() {
    if (this.inputRef) {
      this.inputRef.focus();
    }
  }

  render() {
    return <Child generateRef={this.generateRef}></Child>
  }
}

export default Father;

// 子组件
interface IProps {
  generateRef: (el: HTMLInputElement) => void;
}

class Child extends React.Component<IProps, {}> {

  render() {
    const { generateRef } = this.props;
    return <input type="text" ref={generateRef}/>
  }
}

export default Child;

2、组件内操作DOM

2.1、函数组件——React.createRef
// React.RefObject<HTMLInputElement>
import React, { useEffect } from 'react';

const InputFocus = () => {

  const inputRef = React.createRef<HTMLInputElement>();

  useEffect(() => {
    if (inputRef.current) {
      inputRef.current.focus();
    }
  })

  return <input type="text" ref={inputRef}/>
}

export default InputFocus;
2.2、函数组件——callback ref
// ((instance: HTMLInputElement | null) => void)
import React, { useEffect } from 'react';

const InputFocus = () => {

  let inputRef: HTMLDivElement | null = null;

  useEffect(() => {
    if (inputRef) {
      inputRef.focus();
    }
  })

  return <input type="text" ref={(el) => inputRef = el}/>
}

export default InputFocus;
2.3、类组件——React.createRef
// React.RefObject<HTMLInputElement>
class InputFocus extends React.Component<{}, {}> {
  private inputRef = React.createRef<HTMLInputElement>();

  componentDidMount(){
    if (this.inputRef.current) {
      this.inputRef.current.focus()
    }
  }

  render() {
    return <input type="text" ref={this.inputRef}/>
  }
}

export default InputFocus;
2.4、类组件——callback ref
// ((instance: HTMLInputElement | null) => void)
class InputFocus extends React.Component<{}, {}> {
  private inputRef: HTMLInputElement | null = null;

  componentDidMount(){
    if (this.inputRef) {
      this.inputRef.focus()
    }
  }

  render() {
    return <input type="text" ref={(el) => this.inputRef = el}/>
  }
}

export default InputFocus;

3、使用ref获取组件实例

使用ref获取组件实例只能用在类组件上,因为函数组件没有实例

// 父组件
const Father = () => {
  const ref = useRef<Child | null>(null);
  
  useEffect(() => {
    if (ref.current) {
      ref.current.inputFocus();
    }
  })

  return <Child ref={ref}></Child>
}

export default Father;

// 子组件
class Child extends React.Component<{}, {}> {

  private inputRef = React.createRef<HTMLInputElement>();

  inputFocus = () => {
    if(this.inputRef.current) {
      this.inputRef.current.focus();
    }
  }

  render() {
    return <input type="text" ref={this.inputRef} onFocus={this.inputFocus}></input>
  }
}

export default Child;

参考:
https://juejin.cn/post/684490...


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。