React Ref 其實是這樣的

ref 的由來

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

適合使用 refs 的情況:

  • 管理焦點,文本選擇或媒體播放。

  • 觸發強制動畫。

  • 集成第三方 DOM 庫。

ref 的四種方式

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

ref 通過字符獲取:

<code> 

class

MyComponent

extends

React

.

Component

{ componentDidMount() {

this

.refs.myRef.focus(); } render() {

return

<

input

ref

=

"myRef"

/>

; } }

/<code>

ref 通過回調函數獲取:

<code> 

class

MyComponent

extends

React

.

Component

{ componentDidMount() {

this

.myRef.focus(); } render() {

return

<

input

ref

=

{(ele)

=>

{ this.myRef = ele; }} />; } }

/<code>

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

ref 通過 React.createRef 獲取:

<code> 

class

MyComponent

extends

React

.

Component

{

constructor

(props) {

super

(props);

this

.myRef = React.createRef(); } componentDidMount() {

this

.myRef.current.focus(); } render() {

return

<

input

ref

=

{this.myRef}

/>

; } }

/<code>

當然還有最近react大力推崇的 hooks:useRef

<code>

function

MyComponent

(

)

{

const

myRef = useRef(

null

);

const

onButtonClick =

()

=>

{ myRef.current.focus(); };

return

( <> > ); } /<code>

將被移除的 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 了。 另一方面,回調引用沒有一個所有者,因此您可以隨時編寫它們。例如:

<code> 

class

Parent

extends

React

.

Component

{ componentDidMount() {

console

.log(

this

.refs); } render() {

const

{ children } =

this

.props;

return

React.cloneElement(children, {

ref

:

'childRef'

, }); } }

class

App

extends

React

.

Component

{ componentDidMount() {

console

.log(

this

.refs); } render() {

return

(

<

Parent

>

<

Child

ref

=

"child"

/>

Parent

>

); } }

/<code>
  1. string ref 的所有者由當前執行的組件確定。 這意味著使用通用的“渲染回調”模式(例如react),錯誤的組件將擁有引用(它將最終在react上而不是您的組件定義renderRow)。

<code>

class

MyComponent

extends

Component

{ renderRow = (index) => {

return

'input-' + index} />;

return

this

[

'input-'

+ index] = input} />; } render() {

return

data={

this

.props.

data

} renderRow={

this

.renderRow} /> } } /<code>
  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 組件。

hooks大家族 useRef

這第四種使用 ref 的方法又有何不同呢?

useRef 返回一個可變的 ref 對象,其 .current 屬性被初始化為傳入的參數(initialValue)。返回的 ref 對象在組件的整個生命週期內保持不變。並且 useRef 可以很方便地保存任何可變值,其類似於在 class 中使用實例字段的方式。

正是由於這些特性,useRef 和 createRef 出現了很大差異。

可以運行下以下代碼:

<code>

import

React, { useState, useRef, useEffect }

from

"react"

;

export

default

function

App

(

)

{

const

[count, setCount] = useState(

0

);

const

latestCount = useRef(count); useEffect(

()

=>

{ latestCount.current = count; });

function

handleAlertclick

(

)

{ setTimeout(

()

=>

{ alert(

"latestCount.current:"

+ latestCount.current +

'.. count: '

+ count); },

2000

); }

return

(

<

div

>

<

p

>

當前count: {count}

p

>

<

button

onClick

=

{()

=>

setCount(count + 1)}>count + 1

button

>

<

button

onClick

=

{handleAlertclick}

>

提示

button

>

div

>

) } /<code>

然後按照下面步驟進行操作:

  1. 連續點擊5次 count + 1 按鈕

  2. 點擊 提示 按鈕

  3. 再點擊完 提示 按鈕後2秒內連續點擊2次 count + 1 按鈕

  4. 等待 alert 彈窗提示。

然後會你會得到一個有趣的答案:alert 彈窗會提示: latestCount.current:7.. count: 5。使用 useRef 能獲取到最新的值,但是 useState 卻不能。

具體原因可以參考 react 作者之一 dan 的個人博客。或者查看 React 函數式組件和類組件的區別,不是隻有state和性能!

那麼 useRef 真有那麼很好用嗎?並不是的。還有由於它上面的那個特性,問題還是不少的。

你可以嘗試跑一下下面這段代碼,或者 點擊這裡查看

<code>

import

React, { useRef, createRef, useState }

from

"react"

;

import

ReactDOM

from

"react-dom"

;

function

App

(

)

{

const

[renderIndex, setRenderIndex] = useState(

1

);

const

refFromUseRef = useRef();

const

refFromCreateRef = createRef();

if

(!refFromUseRef.current) { refFromUseRef.current = renderIndex; }

if

(!refFromCreateRef.current) { refFromCreateRef.current = renderIndex; }

return

(

<

div

className

=

"App"

>

Current render index: {renderIndex}

<

br

/>

在refFromUseRef.current中記住的第一個渲染索引: {refFromUseRef.current}

<

br

/>

在refFromCreateRef.current中未能成功記住第一個渲染索引: {refFromCreateRef.current}

<

br

/>

<

button

onClick

=

{()

=>

setRenderIndex(prev => prev + 1)}> 數值 + 1

button

>

div

>

); }

const

rootElement =

document

.getElementById(

"root"

); ReactDOM.render(

<

App

/>

, rootElement);

/<code>

上面的案例中無論如何點擊按鈕 refFromUseRef.current 將始終為 1,而 renderIndex 和 refFromCreateRef.current 會伴隨點擊事件改變; 意想不到吧?

因為:當 ref 對象內容發生變化時,useRef 並不會通知你。變更 .current 屬性不會引發組件重新渲染。如果想要在 React 綁定或解綁 DOM 節點的 ref 時運行某些代碼,則需要使用 callback ref 來實現。

總結下:

  1. useRef 可以獲取 DOM ref

  2. useRef 可以獲取最新的值

  3. useRef 內容發生改變並不會通知

由於上面的一些問題,起初我也是並不想把 useRef 作為操作 ref 的方法來講的。

Refs 轉發

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

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

如何將 ref 暴露給父組件?

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

什麼是 ref 轉發?

<code>

const

FancyButton = React.forwardRef(

(

props, ref

) =>

(

<

button

ref

=

{ref}

className

=

"FancyButton"

>

{props.children}

button

>

));

const

ref = React.createRef();

<

FancyButton

ref

=

{ref}

>

Click me!

FancyButton

>

; /<code>

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

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

比如下面這樣:

<code>function CustomTextInput(props) {
  

return

(

); }

class

Parent

extends

React

.

Component

{

constructor

(props) {

super

(props);

this

.inputElement = React.createRef(); } render() {

return

( this.inputElement} /> ); } } /<code>

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

  1. 我們通過調用 React.createRef 創建了一個 React ref 並將其賦值給 ref 變量。

  2. 我們通過指定 ref 為 JSX 屬性,將其向下傳遞給 。

  3. React 傳遞 ref 給 forwardRef 內函數 (props, ref) => …,作為其第二個參數。

  4. 我們向下轉發該 ref 參數到

  5. 當 ref 掛載完成,ref.current 將指向


React Ref 其實是這樣的


分享到:


相關文章: