React Ref 其實是這樣的

zhongmeizhi發表於2020-10-15

大家好,我是Mokou,好久沒有冒泡了,最近一直在看研究演算法和資料結構方面的東西,但是似乎很多前端不喜歡看這種東西,而且目前本人演算法方面也很挫,就不獻醜了。

當然了,最近也開始研究React了,這篇文章主要是講述 Ref 相關的內容,如有錯誤請指正。

ref 的由來

在典型的 React 資料流中,props 是父元件與子元件互動的唯一方式。要修改一個子元件,你需要使用新的 props 來重新渲染它。但是,在某些情況下,你需要在典型資料流之外強制修改子元件/元素。

適合使用 refs 的情況:

  • 管理焦點,文字選擇或媒體播放。
  • 觸發強制動畫。
  • 整合第三方 DOM 庫。

ref 的三種方式

在 React v16.3 之前,ref 通過字串(string ref)或者回撥函式(callback ref)的形式進行獲取。

ref 通過字元獲取:

// string ref
class MyComponent extends React.Component {
  componentDidMount() {
    this.refs.myRef.focus();
  }

  render() {
    return <input ref="myRef" />;
  }
}

ref 通過回撥函式獲取:

// callback ref
class MyComponent extends React.Component {
  componentDidMount() {
    this.myRef.focus();
  }

  render() {
    return <input ref={(ele) => {
      this.myRef = ele;
    }} />;
  }
}

在 v16.3 中,經 0017-new-create-ref 提案引入了新的 API:React.createRef

ref 通過 React.createRef 獲取:

// React.createRef
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }

  componentDidMount() {
    this.myRef.current.focus();
  }
  
  render() {
    return <input ref={this.myRef} />;
  }
}

將被移除的 string ref

首先來具體說說 string ref,string ref 就已被詬病已久,React 官方文件中如此宣告:"如果你目前還在使用 this.refs.textInput 這種方式訪問 refs ,我們建議用回撥函式或 createRef API 的方式代替。",為何如此糟糕?

最初由 React 作者之一的 dan abramov。釋出於https://news.ycombinator.com/edit?id=12093234,(該網站需要梯子)。吐槽內容主要有以下幾點:

  1. string ref 不可組合。 例如一個第三方庫的父元件已經給子元件傳遞了 ref,那麼我們就無法在在子元件上新增 ref 了。 另一方面,回撥引用沒有一個所有者,因此您可以隨時編寫它們。例如:
/** string ref **/
class Parent extends React.Component {
  componentDidMount() {
    // 可獲取到 this.refs.childRef
    console.log(this.refs);
  }
  render() {
    const { children } = this.props;
    return React.cloneElement(children, {
      ref: 'childRef',
    });
  }
}

class App extends React.Component {
  componentDidMount() {
    // this.refs.child 無法獲取到
    console.log(this.refs);
  }
  render() {
    return (
      <Parent>
        <Child ref="child" />
      </Parent>
    );
  }
}
  1. string ref 的所有者由當前執行的元件確定。 這意味著使用通用的“渲染回撥”模式(例如react),錯誤的元件將擁有引用(它將最終在react上而不是您的元件定義renderRow)。
class MyComponent extends Component {
  renderRow = (index) => {
    // string ref 會掛載在 DataTable this 上
    return <input ref={'input-' + index} />;

    // callback ref 會掛載在 MyComponent this 上
    return <input ref={input => this['input-' + index] = input} />;
  }

  render() {
    return <DataTable data={this.props.data} renderRow={this.renderRow} />
  }
}
  1. string ref 不適用於Flow之類的靜態分析。 Flow不能猜測框架可以使字串ref“出現”在react上的神奇效果,以及它的型別(可能有所不同)。 回撥引用比靜態分析更友好。
  2. string ref 強制React跟蹤當前正在執行的元件。 這是有問題的,因為它使react模組處於有狀態,並在捆綁中複製react模組時導致奇怪的錯誤。在 reconciliation 階段,React Element 建立和更新的過程中,ref 會被封裝為一個閉包函式,等待 commit 階段被執行,這會對 React 的效能產生一些影響。

關於這點可以參考 React 原始碼 coerceRef 的實現:

在調和子節點得過程中,會對 string ref 進行處理,把他轉換成一個方法,這個方法主要做的事情就是設定 instance.refs[stringRef] = element,相當於把他轉換成了function ref

對於更新得過程中string ref是否變化需要對比得是 current.ref._stringRef,這裡記錄了上一次渲染得時候如果使用得是string ref他的值是什麼

owner是在呼叫createElement的時候獲取的,通過ReactCurrentOwner.current獲取,這個值在更新一個元件前會被設定,比如更新ClassComponent的時候,呼叫render方法之前會設定,然後呼叫render的時候就可以獲取對應的owner了。

堅挺的 callback ref

React 將在元件掛載時,會呼叫 ref 回撥函式並傳入 DOM 元素,當解除安裝時呼叫它並傳入 null。在 componentDidMount 或 componentDidUpdate 觸發前,React 會保證 refs 一定是最新的。

如果 ref 回撥函式是以行內函數的方式定義的,在更新過程中它會被執行兩次,第一次傳入引數 null,然後第二次會傳入引數 DOM 元素。這是因為在每次渲染時會建立一個新的函式例項,所以 React 清空舊的 ref 並且設定新的。通過將 ref 的回撥函式定義成 class 的繫結函式的方式可以避免上述問題,但是大多數情況下它是無關緊要的。

最新的 React.createRef

React.createRef 的優點:

  • 相對於 callback ref 而言 React.createRef 顯得更加直觀,避免了 callback ref 的一些理解問題。

React.createRef 的缺點:

  1. 效能略低於 callback ref
  2. 能力上仍遜色於 callback ref,例如上一節提到的組合問題,createRef 也是無能為力的。

ref 的值根據節點的型別而有所不同:

  • 當 ref 屬性用於 HTML 元素時,建構函式中使用 React.createRef() 建立的 ref 接收底層 DOM 元素作為其 current 屬性。
  • 當 ref 屬性用於自定義 class 元件時,ref 物件接收元件的掛載例項作為其 current 屬性。
  • 預設情況下,你不能在函式元件上使用 ref 屬性(可以在函式元件內部使用),因為它們沒有例項:
    • 如果要在函式元件中使用 ref,你可以使用 forwardRef(可與 useImperativeHandle 結合使用)
    • 或者可以將該元件轉化為 class 元件。

Refs 轉發

是否需要將 DOM Refs 暴露給父元件?

在極少數情況下,你可能希望在父元件中引用子節點的 DOM 節點。通常不建議這樣做,因為它會打破元件的封裝,但它偶爾可用於觸發焦點或測量子 DOM 節點的大小或位置。

如何將 ref 暴露給父元件?

如果你使用 16.3 或更高版本的 React, 這種情況下我們推薦使用 ref 轉發。Ref 轉發使元件可以像暴露自己的 ref 一樣暴露子元件的 ref。

什麼是 ref 轉發?

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// 你可以直接獲取 DOM button 的 ref:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

如果在低版本中如何轉發?

如果你使用 16.2 或更低版本的 React,或者你需要比 ref 轉發更高的靈活性,你可以使用 ref 作為特殊名字的 prop 直接傳遞。

比如下面這樣:

function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} />
    </div>
  );
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.inputElement = React.createRef();
  }
  render() {
    return (
      <CustomTextInput inputRef={this.inputElement} />
    );
  }
}

以下是對上述示例發生情況的逐步解釋:

  1. 我們通過呼叫 React.createRef 建立了一個 React ref 並將其賦值給 ref 變數。
  2. 我們通過指定 ref 為 JSX 屬性,將其向下傳遞給 <FancyButton ref={ref}>
  3. React 傳遞 ref 給 forwardRef 內函式 (props, ref) => ...,作為其第二個引數。
  4. 我們向下轉發該 ref 引數到 <button ref={ref}>,將其指定為 JSX 屬性。
  5. 當 ref 掛載完成,ref.current 將指向 <button> DOM 節點。

最後

歡迎關注公眾號「前端進階課」認真學前端,一起進階。回覆 全棧Vue 有好禮相送哦

相關文章