React 源碼漂流(四)之 createRef

引言

本篇從 React Refs 的使用場景、使用方式、注意事項,到 createRef 與 Hook useRef 的對比使用,最後以 React createRef 源碼結束,剖析整個 React Refs,關於 React.forwardRef 會在下一篇文章深刻探討。前端

1、Refs

React 的核心思想是每次對於界面 state 的改動,都會從新渲染整個Virtual DOM,而後新老的兩個 Virtual DOM 樹進行 diff(協調算法),對比出變化的地方,而後經過 render 渲染到實際的UI界面,node

使用 Refs 爲咱們提供了一種繞過狀態更新和從新渲染時訪問元素的方法;這在某些用例中頗有用,但不該該做爲 propsstate 的替代方法。react

在項目開發中,若是咱們可使用 聲明式 或 提高 state 所在的組件層級(狀態提高) 的方法來更新組件,最好不要使用 refs。git

使用場景

  • 管理焦點(如文本選擇)或處理表單數據: Refs 將管理文本框當前焦點選中,或文本框其它屬性。github

    在大多數狀況下,咱們推薦使用受控組件來處理表單數據。在一個受控組件中,表單數據是由 React 組件來管理的,每一個狀態更新都編寫數據處理函數。另外一種替代方案是使用非受控組件,這時表單數據將交由 DOM 節點來處理。要編寫一個非受控組件,就須要使用 Refs 來從 DOM 節點中獲取表單數據。算法

    class NameForm extends React.Component {
      constructor(props) {
        super(props);
        this.input = React.createRef();
      }
    
      handleSubmit = (e) => {
        console.log('A name was submitted: ' + this.input.current.value);
        e.preventDefault();
      }
    
      render() {
        return (
          <form onSubmit={this.handleSubmit}>
            <label>
              Name:
              <input type="text" ref={this.input} />
            </label>
            <input type="submit" value="Submit" />
          </form>
        );
      }
    }
    複製代碼

    由於非受控組件將真實數據儲存在 DOM 節點中,因此再使用非受控組件時,有時候反而更容易同時集成 React 和非 React 代碼。若是你不介意代碼美觀性,而且但願快速編寫代碼,使用非受控組件每每能夠減小你的代碼量。不然,你應該使用受控組件。數組

  • 媒體播放: 基於 React 的音樂或視頻播放器能夠利用 Refs 來管理其當前狀態(播放/暫停),或管理播放進度等。這些更新不須要進行狀態管理。安全

  • 觸發強制動畫: 若是要在元素上觸發過強制動畫時,可使用 Refs 來執行此操做。函數

  • 集成第三方 DOM 庫動畫

使用方式

Refs 有 三種實現:

一、方法一:經過 createRef 實現

createRef 是 **React v16.3 ** 新增的API,容許咱們訪問 DOM 節點或在 render 方法中建立的 React 元素。

Refs 是使用 React.createRef() 建立的,並經過 ref 屬性附加到 React 元素。

Refs 一般在 React 組件的構造函數中定義,或者做爲函數組件頂層的變量定義,而後附加到 render() 函數中的元素。

export default class Hello extends React.Component {
  constructor(props) {
    super(props);
    // 建立 ref 存儲 textRef DOM 元素
    this.textRef = React.createRef(); 
  }
  componentDidMount() {
    // 注意:經過 "current" 取得 DOM 節點
    // 直接使用原生 API 使 text 輸入框得到焦點
    this.textRef.current.focus(); 
  }
  render() {
    // 把 <input> ref 關聯到構造器裏建立的 textRef 上
    return <input ref={this.textRef} /> } } 複製代碼

使用 React.createRef() 給組件建立了 Refs 對象。在上面的示例中,ref被命名 textRef,而後將其附加到 <input> DOM元素。

其中, textRef 的屬性 current 指的是當前附加到 ref 的元素,並普遍用於訪問和修改咱們的附加元素。事實上,若是咱們經過登陸 myRef 控制檯進一步擴展咱們的示例,咱們將看到該 current 屬性確實是惟一可用的屬性:

componentDidMount = () => {
   // myRef 僅僅有一個 current 屬性
   console.log(this.textRef);
   // myRef.current
   console.log(this.textRef.current);
   // component 渲染完成後,使 text 輸入框得到焦點
   this.textRef.current.focus();
}
複製代碼

componentDidMount 生命週期階段,myRef.current 將按預期分配給咱們的 <input> 元素;  componentDidMount 一般是使用 refs 處理一些初始設置的安全位置。

咱們不能在 componentWillMount 中更新 Refs,由於此時,組件還沒渲染完成, Refs 還爲 null

二、方法二:回調 Refs

不一樣於傳遞 createRef() 建立的 ref 屬性,你會傳遞一個函數。這個函數中接受 React 組件實例或 HTML DOM 元素做爲參數,以使它們能在其餘地方被存儲和訪問。

import React from 'react';
export default class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.textRef = null; // 建立 ref 爲 null
  }
  componentDidMount() {
    // 注意:這裏沒有使用 "current" 
    // 直接使用原生 API 使 text 輸入框得到焦點
    this.textRef.focus(); 
  }
  render() {
    // 把 <input> ref 關聯到構造器裏建立的 textRef 上
    return <input ref={node => this.textRef = node} /> } } 複製代碼

React 將在組件掛載時將 DOM 元素傳入ref 回調函數並調用,當卸載時傳入 null 並調用它。在 componentDidMount 或 componentDidUpdate 觸發前,React 會保證 refs 必定是最新的。

像上例, ref 回調函數是之內聯函數的方式定義的,在更新過程當中它會被執行兩次,第一次傳入參數 null,而後第二次會傳入參數 DOM 元素。

這是由於在每次渲染時會建立一個新的函數實例,因此 React 清空舊的 ref 而且設置新的。咱們能夠經過將 ref 的回調函數定義成 class 的綁定函數的方式能夠避免上述問題,可是大多數狀況下它是可有可無的。

三、方法三:經過 stringRef 實現
export default class Hello extends React.Component {
  constructor(props) {
    super(props);
  }
  componentDidMount() {
    // 經過 this.refs 調用
    // 直接使用原生 API 使 text 輸入框得到焦點
    this.refs.textRef.focus(); 
  }
  render() {
    // 把 <input> ref 關聯到構造器裏建立的 textRef 上
    return <input ref='textRef' /> } } 複製代碼

儘管字符串 stringRef 使用更方便,可是它有一些缺點,所以嚴格模式使用 stringRef 會報警告。官方推薦採用回調 Refs。

注意

  • ref 屬性被用於一個普通的 HTML 元素時,React.createRef() 將接收底層 DOM 元素做爲它的 current 屬性以建立 ref ,咱們能夠經過 Refs 訪問 DOM 元素屬性。
  • ref 屬性被用於一個自定義 class 組件時,ref 對象將接收該組件已掛載的實例做爲它的 current,與 ref 用於 HTML 元素不一樣的是,咱們可以經過 ref 訪問該組件的props,state,方法以及它的整個原型 。
  • ref 是爲了獲取某個節點是實例,因此 你不能在函數式組件上使用 ref 屬性,由於它們沒有實例。
  • 推薦使用 回調形式的 refs, stringRef 將會廢棄(嚴格模式下使用會報警告),React.createRef() API 是 React v16.3 引入的更新。
  • 避免使用 refs 來作任何能夠經過 聲明式 實現來完成的事情

2、createRef 與 Hook useRef

useRef

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化爲傳入的值。返回的 ref 對象在組件的整個生命週期內保持不變。

function Hello() {
  const textRef = useRef(null)
  const onButtonClick = () => {
    // 注意:經過 "current" 取得 DOM 節點
    textRef.current.focus();
  };
  return (
    <>
      <input ref={textRef} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  )
}
複製代碼

####區別

useRef()ref 屬性更有用。useRef() Hook 不只能夠用於 DOM refs, useRef() 建立的 ref 對象是一個 current 屬性可變且能夠容納任意值的通用容器,相似於一個 class 的實例屬性。

function Timer() {
  const intervalRef = useRef();

  useEffect(() => {
    const id = setInterval(() => {
      // ...
    });
    intervalRef.current = id;
    return () => {
      clearInterval(intervalRef.current);
    };
  });

  // ...
}
複製代碼

這是由於它建立的是一個普通 Javascript 對象。而 useRef() 和自建一個 {current: ...}對象的惟一區別是,useRef 會在每次渲染時返回同一個 ref 對象。

請記住,當 ref 對象內容發生變化時,useRef不會通知你。變動 .current 屬性不會引起組件從新渲染。若是想要在 React 綁定或解綁 DOM 節點的 ref 時運行某些代碼,則須要使用回調 ref 來實現。

3、createRef 源碼解析

// ReactCreateRef.js 文件
import type {RefObject} from 'shared/ReactTypes';

// an immutable object with a single mutable value
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  if (__DEV__) {
    // 封閉對象,阻止添加新屬性並將全部現有屬性標記爲不可配置。當前屬性的值只要可寫就能夠改變。
    Object.seal(refObject); 
  }
  return refObject;
}
複製代碼

其中 RefObject 爲:

export type RefObject = {|
  current: any,
|};
複製代碼

這就是的 createRef 源碼,實現很簡單,但具體的它如何使用,如何掛載,將在後面的 React 渲染中介紹,敬請期待。

系列文章

想看更過系列文章,點擊前往 github 博客主頁

走在最後,歡迎關注:前端瓶子君,每日更新

前端瓶子君
相關文章
相關標籤/搜索