什么是ref
Refs 提供了一種方式,允許我們?cè)L問(wèn) DOM 節(jié)點(diǎn)或在 render 方法中創(chuàng)建的 React 元素
上面是官網(wǎng)對(duì)ref
的介紹,簡(jiǎn)單概括一下ref的作用為用來(lái)獲取組件的實(shí)例或Dom,并且無(wú)論是你使用Vue框架還是React框架,都不建議過(guò)度使用ref
,能用組件通信來(lái)解決的問(wèn)題,一般不推薦使用ref
,一般是作為“逃生艙”來(lái)使用,但有一些情況,你不得不使用ref
獲取組件的實(shí)例或者DOM,來(lái)打破典型的數(shù)據(jù)流形式組件通信。比如,我們做了某些比較死的表單封裝,想直接通過(guò)父組件調(diào)用其提交方法,比如,你想讓你封裝的“輪播圖”組件直接執(zhí)行其下一步的操作,等等等,程序員可能遇到很多種奇怪的需求,也可能需要你用到ref
,這里發(fā)表一下個(gè)人觀點(diǎn),相對(duì)于Vue這種漸進(jìn)式框架而言,React從一開(kāi)始就對(duì)開(kāi)發(fā)者提出的比較嚴(yán)格規(guī)范的要求,所以React的ref并不像Vue中那樣出現(xiàn)的平凡,多數(shù)情況下,還是依照經(jīng)典的數(shù)據(jù)流來(lái)完成一些操作,雖然兩個(gè)兩個(gè)框架都不建議過(guò)度使用ref
,比較低的出場(chǎng)率也就注定在使用到它的時(shí)候難免遇到一些坑,今天,我將通過(guò)這篇文章,來(lái)盡可能詳細(xì)的介紹ref的相關(guān)內(nèi)容,并記錄我使用React + ts 訪問(wèn)ref
時(shí)遇到的一些坑(本人,react老玩家,ts實(shí)屬新手)。
環(huán)境準(zhǔn)備
- create-react-app
- typescript
ref的訪問(wèn)方式
- React.createRef()
- useRef(只在函數(shù)組件中使用的hooks)
- 回調(diào)函數(shù)
- 字符串(已廢棄,不要再使用了!)
ref 的值根據(jù)節(jié)點(diǎn)的類型而有所不同:
- 當(dāng)
ref
屬性用于 HTML 元素時(shí),ref
為其底層 DOM 元素。 - 當(dāng)
ref
屬性用于自定義 class 組件時(shí),ref 對(duì)象為其接收組件的掛載實(shí)例。 -
你不能在函數(shù)組件上使用 ref 屬性,因?yàn)樗麄儧](méi)有實(shí)例。如果你想要在函數(shù)組件中使用
ref
,可以使用forwardRef
,但你可以在函數(shù)組件內(nèi)部使用ref屬性,只要他是指向DOM元素或者class組件。
上面介紹了幾個(gè)關(guān)鍵點(diǎn),下面,我們將就上面提到的點(diǎn),做詳細(xì)的介紹和實(shí)例demo。
React.createRef()
app.tsx
class App extends React.PureComponent {
childRef: any
constructor (props:any) {
super(props)
this.childRef = React.createRef()
console.log(this.childRef.current)
}
render () {
return (
<div className="App">
這是一個(gè)類組件
<Child ref={this.childRef}/>
</div>
);
}
componentDidMount () {
console.log(this.childRef.current)
}
}
export default App;
child.tsx
import React from 'react'
class Child extends React.PureComponent {
render () {
return <div>這是子組件</div>
}
}
export default Child
上面,我們使用React.createRef()
,在類組件App中訪問(wèn)了類組件Child
的ref。我們通過(guò)React.createRef()
創(chuàng)建refs
,并通過(guò)ref
屬性,傳給對(duì)應(yīng)的子組件,因?yàn)樽咏M件是一個(gè)類組件,那么就會(huì)將其實(shí)例,掛載到ref對(duì)象的current
屬性上,于是我們的打印結(jié)果是這樣的。
結(jié)果比較明顯了,這里有一個(gè)細(xì)節(jié)是,我寫(xiě)在
constructor
中的打印結(jié)果為null,而寫(xiě)在componentDidMount
生命周期里才能正常打印,所以,這里有一點(diǎn)需要主要的是ref是組件或者DOM掛載后才可以訪問(wèn)到的,這一點(diǎn)需要注意,你在訪問(wèn)組件ref時(shí),必須確保其已經(jīng)掛載。我們上面的代碼中,APP也是一個(gè)類組件,那么如果APP是函數(shù)組件呢?我們也是可以正常使用
React.createRef()
,只不過(guò)純函數(shù)組件沒(méi)有生命周期,我們可以通過(guò)事件來(lái)訪問(wèn)(這時(shí)候組件一定是掛載完成的),通常將 Refs 分配給實(shí)例屬性,以便可以在整個(gè)組件中引用它們。
const App: React.FC = () => {
const childRef: any = React.createRef()
const getRef = () => {
console.log(childRef.current)
}
return (
<div className="App">
這是一個(gè)函數(shù)組件
<Child ref={childRef}/>
<button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
</div>
);
}
export default App
我們也是可以正常獲取結(jié)果的。其實(shí)對(duì)于純函數(shù)組件,我更推薦使用useRef
這個(gè)hooks來(lái)完成,因?yàn)?a target="_blank">useRef
的優(yōu)勢(shì)還是比傳統(tǒng)的獲取ref的形式要多很多,因?yàn)閡seRef不僅僅可以存ref,還可以存任何值,你可以用它來(lái)存變量等,當(dāng)然,這是這個(gè)hooks本身的優(yōu)勢(shì),今天我們主要說(shuō)ref,還是說(shuō)說(shuō)怎么使用useRef
來(lái)訪問(wèn)ref吧
useRef
react推出hooks可謂是讓react變得更受歡迎了,也更加的舒服了,其中的refRef
就可以幫助我們?cè)诩兒瘮?shù)組件中訪問(wèn)refs對(duì)象,于是,對(duì)于上面,我們使用react.createRef()
在純函數(shù)中訪問(wèn)refs的代碼,可以做下面的更改
import React, { useRef } from 'react';
const App: React.FC = () => {
const childRef: any = useRef(null)
const getRef = () => {
console.log(childRef.current)
}
return (
<div className="App">
這是一個(gè)函數(shù)組件
<Child ref={childRef}/>
<button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
</div>
);
}
export default App
其結(jié)果也是一樣的,useRef
同React.createRef()
類似,都會(huì)將ref放在其.current
屬性中,不過(guò),useRef
的作用要遠(yuǎn)遠(yuǎn)大于后者,這里推薦,再次推薦。具體可看官網(wǎng)對(duì)其的介紹。
回調(diào)函數(shù)
React 也支持另一種設(shè)置 refs 的方式,稱為“回調(diào) refs”。它能助你更精細(xì)地控制何時(shí) refs 被設(shè)置和解除。不同于react.createRef
和useRef
返回一個(gè)對(duì)象,如果想要在 React 綁定或解綁 DOM 節(jié)點(diǎn)的 ref 時(shí)運(yùn)行某些代碼,則需要使用回調(diào)ref來(lái)實(shí)現(xiàn)。
我們還是使用上面的代碼(實(shí)際開(kāi)發(fā)中,我已經(jīng)很少寫(xiě)類組件了~),換成回調(diào)函數(shù)的形式。
import React, { useState } from 'react';
import Child from './components/child'
const App: React.FC = () => {
const [isMount, setIsMount] = useState(true)
let childRef: any
const getRef = () => {
console.log(childRef)
}
const setRef = (node: any) => {
console.log('我掛載/卸載了')
childRef = node
}
const unMountChild = () => {
setIsMount(false)
}
return (
<div className="App">
這是一個(gè)函數(shù)組件
{isMount && <Child ref={setRef}/>}
<button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
<button onClick={unMountChild}>點(diǎn)擊卸載子組件</button>
</div>
);
}
export default App
在上面的代碼中,我們通過(guò)傳入回調(diào)的方式,將refs對(duì)象賦給了childRef變量,并可以在某一事件中獲取它,這一點(diǎn),和其他兩種方式?jīng)]有差別,差別在于,我們可以在Child組件掛載或者卸載的時(shí)候執(zhí)行一些方法,我們通過(guò)一個(gè)狀態(tài)控制了子組件的掛載狀態(tài),來(lái)做這個(gè)demo,最終的實(shí)際效果如下
我們?cè)趧傞_(kāi)始掛載時(shí)執(zhí)行了方法,這時(shí)候我們通過(guò)事件獲取其refs對(duì)象,當(dāng)修改狀態(tài)使組件卸載,可以看到再次執(zhí)行了方法,并且這時(shí)候也獲取不到refs對(duì)象了。這就是refs回調(diào)的作用。
小結(jié):上面我們介紹了幾種訪問(wèn)refs對(duì)象和創(chuàng)建refs對(duì)象的方法,這樣的文章在網(wǎng)上也是層出不窮,不新鮮,如果你只是想簡(jiǎn)單學(xué)習(xí)怎么去創(chuàng)建和訪問(wèn)refs那么你可以看到這里就好了,秉承著我一貫的愛(ài)踩坑風(fēng)格,我決定讓自己走一些彎路,再去探索一下更“奇葩”的用法,并且其極有可能在你的業(yè)務(wù)中用到。
訪問(wèn)DOM的ref對(duì)象
上面,我們的Child是一個(gè)類組件,其存在實(shí)例,于是我們通過(guò)三種方式,訪問(wèn)到了這個(gè)實(shí)例,那么我們思考,如果我們?cè)L問(wèn)的不是一個(gè)類組件,而是一個(gè)普通DOM節(jié)點(diǎn),會(huì)是什么結(jié)果呢?我們?cè)囈幌隆?/p>
import React, { useRef } from 'react';
const App: React.FC = () => {
const childRef: any = useRef(null)
const getRef = () => {
console.log(childRef.current)
}
return (
<div className="App">
這是一個(gè)函數(shù)組件
<div ref={childRef}>這是一個(gè)普通DOM節(jié)點(diǎn)<span>我也是</span></div>
<button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
</div>
);
}
export default App
結(jié)果是這樣的
所以說(shuō):
當(dāng) ref 屬性用于 HTML 元素時(shí),創(chuàng)建的 ref 接收底層 DOM 元素作為其 current 屬性。
其效果和document.getElementById
一樣。但通常,我們很少在react框架中使用document.getElementById這樣的語(yǔ)法,那么你就用ref吧!
訪問(wèn)純函數(shù)組件的ref對(duì)象(ref轉(zhuǎn)發(fā))
在文章的上面,我們說(shuō)過(guò),“你只能訪問(wèn)class組件的ref,因?yàn)榧兒瘮?shù)組件沒(méi)有實(shí)例,但如果你非要獲取純函數(shù)組件的ref,你可以使用React.forwardRef
”,我們先來(lái)試一下,正常訪問(wèn)純函數(shù)組件的Ref會(huì)出現(xiàn)什么情況。我們先將Child組件改成純函數(shù)的形式
import React from 'react'
const Child: React.FC = (props: any) => {
return (
<div>
這是一個(gè)子組件
</div>
)
}
export default Child
我們將Child變成了一個(gè)常見(jiàn)的,但是當(dāng)我們直接去訪問(wèn)其Ref時(shí),就會(huì)報(bào)這樣的錯(cuò)誤(強(qiáng)大的ts),當(dāng)然,如果你使用的是js,也會(huì)在執(zhí)行的過(guò)程中報(bào)錯(cuò)一些明顯的錯(cuò)誤。
上面的意思是,函數(shù)組件并不能訪問(wèn)ref,如果我們非要訪問(wèn)怎么辦?這個(gè)時(shí)候就會(huì)用到
forwardRef
了,如果你在js中使用過(guò)forwardRef
,你會(huì)知道,使用forwardRef
包裝后的純函數(shù)組件第二個(gè)參數(shù)為 ref
就像這樣
const Child = (props, ref) => ...
export default React.forwardRef(Child)
但,我們?cè)趖s中使用時(shí),我就踩到了第一個(gè)坑,ts包出這樣的類型錯(cuò)誤
研究后發(fā)現(xiàn),我們不能將React.FC類型傳給
forwardRef
,他需要的是一個(gè)ForwardRefRenderFunction
類型,查看ForwardRefRenderFunction
類型用法后,我們做出修改如下。
import React from 'react'
const Child: React.ForwardRefRenderFunction<unknown, {}> = (props: any, ref: any) => {
return (
<div ref={ref}>
這是一個(gè)子組件
</div>
)
}
export default React.forwardRef(Child)
import React, { useRef } from 'react';
import Child from './components/child'
const App: React.FC = () => {
const childRef: any = useRef(null)
const getRef = () => {
console.log(childRef.current)
}
return (
<div className="App">
這是一個(gè)函數(shù)組件
<Child ref={childRef}/>
<button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
</div>
);
}
export default App
這樣我們就可以找到正常訪問(wèn)了
上面我們做了這樣的操作
- 在父組件中創(chuàng)建了refs對(duì)象,并向下傳遞至Child組件
- 子組件通過(guò)
forwardRef
的第二個(gè)參數(shù)接收ref - 子組件將接收的
ref
傳至對(duì)應(yīng)的DOM節(jié)點(diǎn)或者類組件上,甚至也可以是函數(shù)組件上,就重復(fù)上面的操作 - 在父組件中可以訪問(wèn)到子組件的DOM節(jié)點(diǎn)或者其某個(gè)組件的實(shí)例。
因?yàn)楹瘮?shù)組件并沒(méi)有實(shí)例,所以,我們只能通過(guò)訪問(wèn)函數(shù)子組件的ref而訪問(wèn)到其下的其他節(jié)點(diǎn)或者實(shí)例,我們也叫這種操作稱為ref轉(zhuǎn)發(fā)。ref轉(zhuǎn)發(fā)實(shí)現(xiàn)了一種將子組件DOM節(jié)點(diǎn)暴露給父組件的,提到將DOM節(jié)點(diǎn)暴露給父組件,除了ref轉(zhuǎn)發(fā),還有有一種ref回調(diào)的形式。下面再介紹一下這種方法。
使用ref回調(diào)將DOM節(jié)點(diǎn)暴露給父組件
看標(biāo)題可能比較懵逼,但其實(shí)原理很簡(jiǎn)單,那就是react的父子組件通信。下面用代碼來(lái)演示一下
import React, { useRef } from 'react';
import Child from './components/child'
const App: React.FC = () => {
let childRef: any
const getRef = () => {
console.log(childRef.current)
}
const setRef = (node: any) => {
childRef = node
}
return (
<div className="App">
這是一個(gè)函數(shù)組件
<Child childRef={setRef}/>
<button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
</div>
);
}
export default App
我們先傳一個(gè)回調(diào)props給子組件,這時(shí)候遇到了一坑
在js中,這一套操作肯定是行云流水的一套基操,但在ts有了類型約束后,我們不能隨便往子組件里面?zhèn)饕恍傩粤耍枰谧咏M件中定義props的類型。所以,這里插播一條內(nèi)容
在ts中定義子組件props類型
子組件可能是函數(shù)組件也可能是class組件,我們分別來(lái)演示一下如果定義其props類型
import React from 'react'
interface childProps {
childRef?: (node:any) => void
}
const Child: React.ForwardRefRenderFunction<unknown, childProps> = (props: any, ref: any) => {
console.log(props)
return (
<div ref={ref}>
這是一個(gè)子組件
</div>
)
}
export default React.forwardRef(Child)
這樣我們?cè)诟附M件中傳遞props時(shí)也不會(huì)報(bào)錯(cuò)了,也能順利傳遞自定義props了。
當(dāng)然了,我們也可以在
React.FC
泛型中定義props類型。
import React from 'react'
interface childProps {
childRef?: (node:any) => void
}
const Child: React.FC<childProps> = (props: any) => {
console.log(props)
return (
<div>
這是一個(gè)子組件
</div>
)
}
export default Child
結(jié)果也是一樣的,我們還可以在class組件中定義。
import React from 'react'
interface childProps<T> {
childRef?: T
}
class Child<T> extends React.PureComponent<childProps<T>> {
render () {
console.log(this.props)
return <div>這是子組件</div>
}
}
export default Child
或者
import React from 'react'
interface childProps {
childRef?: (node: any) => void
}
class Child extends React.PureComponent<childProps> {
render () {
console.log(this.props)
return <div>這是子組件</div>
}
}
export default Child
后者更精確類型。
了解了上面的內(nèi)容后,我們就可以自由的向子組件中傳遞props了。然后回到主題上來(lái),接著研究我們的使用ref回調(diào)的形式將DOM暴露給父組件。這時(shí)就輕車熟路了。我習(xí)慣將組件盡量使用精簡(jiǎn)的純函數(shù)形式,下面來(lái)寫(xiě)純函數(shù)
import React from 'react'
interface childProps {
childRef?: (node:any) => void
}
const Child: React.FC<childProps> = (props: any) => {
return (
<div ref={props.childRef}>
這是一個(gè)子組件
</div>
)
}
export default Child
這時(shí)候,你父組件中的就可以這樣獲取子組件暴露的DOM了。
import React from 'react';
import Child from './components/child'
const App: React.FC = () => {
let childRef: any
const getRef = () => {
console.log(childRef) // 在這里獲取,注意不是在.current屬性中了。因?yàn)槲覀冇玫氖腔卣{(diào)。這里容易手滑
}
const setRef = (node: any) => {
childRef = node
}
return (
<div className="App">
這是一個(gè)函數(shù)組件
<Child childRef={setRef}/>
<button onClick={getRef}>點(diǎn)擊獲取組件ref</button>
</div>
);
}
export default App
結(jié)果也是一如既往哦
寫(xiě)在后面
本文到這里就結(jié)束了,大部分代碼比較相似,但是也是全部貼了出來(lái),為了做更完成的記錄,和盡可能詳細(xì)的講解。本文以react+ts為基礎(chǔ),探索react的ref,詳細(xì)的介紹了Ref的各種使用場(chǎng)景,和在ts類型約束下,可能遇到的坑。