
壹 ❀ 引
在前面的文章中,我們介紹了react
的狀態提升,隨之引出了redux
以及context
,其實都說到底都是為了方便管理react
的狀態,讓數據交互與組件通信變得更為簡單。我們知道,react
屬於單向數據流,屬性方法都像瀑布的水往下層組件流動,子組件獲取父組件的屬性也很簡單,一個props
就能搞定。問題來了,那萬一父組件需要獲取子組件的屬性方法?或者父組件需要直接操作子組件的DOM,這又該如何下手呢?這里就不得不提react中的refs
屬性,本篇文章將圍繞ref
用法(三種寫法)以及ref
轉發(傳遞)展開,文中的例子推薦復制后運行,了解下大致運轉過程總是好的,那么本文開始。
貳 ❀ Refs基本用法
在react
的16.3版本引入了新的refs
創建模式,如果要使用refs
我們都推薦使用React.createRef
或者函數回調模式,下面的例子也會使用React.createRef
模式來介紹相關用法,當然對於另外兩種模式(回調與字符串)后面也會介紹,以下例子還是基於create-react-app
項目,所以大家可以在文中提到的對應文件進行代碼修改,然后本地運行項目即可。
貳 ❀ 壹 ref + DOM
react
提供了React.createRef
來創建一個ref
,然后將此ref
屬性附加到你想操作的DOM以及想獲取屬性方法的子組件上,我們在index.js
文件中添加如下代碼:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Parent extends Component {
constructor(props) {
super(props);
// 創建一個ref,這個ref隨便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount(){
console.log(this.echoRef);
}
render() {
// 這里的ref就是必須這么寫了,通過ref屬性將this.echoRef與子組件關聯
return <div ref={this.echoRef}>你好啊,echo。</div>
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
控制台執行npm start
運行項目,我們提前在componentDidMount
中輸出了this.echoRef
,打開控制台,可以看到輸出的是一個對象,對象有個current
屬性,展開后發現current
指向的就是我們添加了this.echoRef
的div
元素。所以可以得知,通過this.refName.current
能訪問到添加了ref
屬性的DOM元素或者組件。
我們修改上述componentDidMount
中的輸出代碼為this.echoRef.current.innerHtml
,打開控制台:

那么問題來了,假設我創建了一個ref
,用在了多個元素上會怎么樣呢?這里我們修改render
內部的代碼為:
render() {
return (
<Fragment>
<div ref={this.echoRef}>你好啊,echo。</div>
<div ref={this.echoRef}>你好啊,時間跳躍。</div>
</Fragment>
)
}

保存后查看控制台,你會發現只有后面div
的生效了,也就說,ref
的綁定就像一個同名的變量賦值,它永遠以最后關聯的DOM為准,一個ref
只能關聯一個,無法重復使用。假設需要關聯多個,我們完全可以創建多個ref
,這一點大家可以自行嘗試。
關於上述中,我們使用了Fragment
組件,在JavaScript中其實也有DocumentFragment
相關概念,意思就是最小文檔片段。我們知道render
中只接受一個根元素作為最外層DOM,比如如下代碼就會報錯:
render() {
return (
<div ref={this.echoRef}>你好啊,echo。</div>
<div ref={this.echoRef}>你好啊,時間跳躍。</div>
)
}
傳統做法是添加一個公有的父級div
將其包裹起來,比如:
render() {
return (
<div>
<div ref={this.echoRef}>你好啊,echo。</div>
<div ref={this.echoRef}>你好啊,時間跳躍。</div>
</div>
)
}
但這樣會產生一層無意義的div
結構,雖然對於DOM優化來說影響微乎其微,但能少一層總是好的,我們打開控制台查看Fragment
包裹的html
結構,如圖:

你會發現Fragment
並沒有產生多余的DOM
結構,如果你了解過vue
或者微信小程序,react
的Fragment
對標vue
的template
與小程序的block
標簽,題外話說到這里。
貳 ❀ 貳 ref + Class組件
前面的例子我們將ref
加在了DOM
上,現在我們試試加在一個子組件上,修改index.js
代碼為:
import React, { Component, Fragment } from 'react';
import ReactDOM from 'react-dom';
class Parent extends Component {
constructor(props) {
super(props);
// 創建一個ref,這個ref隨便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
// 這里調用了子組件的方法
this.echoRef.current.handleClick();
}
render() {
return (
<Children ref={this.echoRef} userName="echo" />
)
}
}
class Children extends Component {
constructor(props) {
super(props);
}
state = {
userName: '聽風是風'
}
// 這個方法給父組件調用
handleClick = () => { console.log('我在調用子組件的方法。') }
render() {
return (
<div>你好,我是{this.props.userName}。</div>
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);

打開控制台,你會發現當前this.echoRef.current
訪問到的就是我們的Children
組件,而且你能看到Children
上聲明的state
以及方法,我們通過this.echoRef.current.handleClick()
調用了子組件的方法,因此控制台輸出了我在調用子組件的方法。
。
ref
不僅僅能獲取子組件的屬性,同樣能像鏈式讀取那樣訪問到自己的孫子組件,以及更下層的組件的屬性,現在我們再嵌套一層組件Grandson
,如下:
import React, { Component, Fragment } from 'react';
import ReactDOM from 'react-dom';
class Parent extends Component {
constructor(props) {
super(props);
// 創建一個ref,這個ref隨便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
// 這里調用了孫子組件的方法
this.echoRef.current.timeStepRef.current.handleClick();
}
render() {
return (
<Children ref={this.echoRef} userName="echo" />
)
}
}
class Children extends Component {
constructor(props) {
super(props);
this.timeStepRef = React.createRef();
}
state = {
userName: '聽風是風'
}
// 這個方法給父組件調用
handleClick = () => { console.log('我在調用子組件的方法。') }
render() {
return (
<Grandson ref={this.timeStepRef} />
)
}
}
class Grandson extends Component {
constructor(props) {
super(props)
}
// 這個方法給祖父組件使用
handleClick = () => { console.log('我是給上上層組件使用的方法') }
render() {
return (
<div>你好,我是孫子組件。</div>
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);

如上圖的對象解構,我們在父級組件通過this.echoRef.current.timeStepRef.current.handleClick();
調用了孫子組件的方法,所以不管組件嵌套多少層,只有有定義ref
你就一定能向下訪問到你想要的屬性,當然,這種做法想想就知道非常不好!
貳 ❀ 叄 ref + 函數組件以及ref轉發
默認情況來說,ref
不能添加在函數組件上,因為函數組件沒有實例,如果你按照前面的做法代碼會給出警告,比如下面這個例子:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
function Children () {
return <div>我是子組件</div>;
}
class Parent extends Component {
constructor(props) {
super(props);
// 創建一個ref,這個ref隨便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
}
render() {
return (
<Children ref={this.echoRef} userName="echo" />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
Warning:Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
但是呢,我們是可以在函數組件內,為其它DOM
或者組件綁定ref
,比如這個例子:
import React, { createRef } from 'react';
import ReactDOM from 'react-dom';
function Children(props, ref) {
const inputRef =createRef();
const handleClick = ()=>{
inputRef.current.focus();
}
return (
<>
<input ref={inputRef} />
<button onClick={handleClick}>點我讓輸入框聚焦</button>
</>
);
}
ReactDOM.render(
<Children />,
document.getElementById('root')
);

綜合來解釋,默認情況下,函數組件內部可以使用ref
去綁定其它的DOM或者組件,但是如果你要直接給一個子函數組件上添加ref
就會出現上面的警告,警告中也給出了解決方案,那就是使用React.forwardRef
。
PS:上述代碼中出現的<></>
標簽作用與Fragment
作用相同,這個做個補充。
我們先來看看如何能在函數組件上添加ref
屬性而不報警告,看下面這個例子:
import React, { Component, forwardRef, useRef, useImperativeHandle, createRef } from 'react';
import ReactDOM from 'react-dom';
function Children(props, ref) {
// useRef是一個hook,你只用知道它可以創建ref
const inputRef = useRef();
// 你可以通過這種方式創建
// const inputRef = createRef()
const sayName = () => {
console.log(1);
}
useImperativeHandle(ref, () => ({
focus: () => {
// 這里操作的是input自帶的focus方法
inputRef.current.focus();
}
}));
return <input ref={inputRef} />;
}
// 由於函數組件不能用ref,這里使用`forwardRef`包裹了一層
Children = forwardRef(Children);
class Parent extends Component {
constructor(props) {
super(props);
// 創建一個ref,這個ref隨便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.log(this.echoRef);
this.echoRef.current.focus();
}
render() {
return (
<Children ref={this.echoRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
一下子出現了很多奇奇怪怪的方法,別急,我們來解釋它們。現在你不用了解這些方法的原理,你只用大致知道它們做了什么,深入的學習應該在更后面,因為這里涉及到了hook
的知識。
react
在16.8版本新增了hook
特性,准確來說,hook
顛覆了我們之前Class
組件以及生命周期那一套。hook
中沒有生命周期,所有的組件都是函數組件,很明顯,函數組件內是沒有this
的,那自然沒辦法從父級組件訪問函數組件this(組件實例)
上的屬性方法。但是假設現在我們就是要操作函數組件的DOM
,比如上面的例子。
我們來解釋上面的代碼,首先forwardRef
接受一個函數,此函數支持2個參數props與ref
,props
很好理解,就是上層傳遞下來的屬性,而ref
自然也是上層傳遞下來的ref
。比較巧的是我們的Children
本身就是個函數,因此我們直接使用forwardRef
進行了包裹,可以理解為給組件升了個級。
而在Children
內部const inputRef = useRef()
這一句,也是使用hook
提供的API
創建一個ref
,他與createRef
的原理以及含義上有一定差異,不過這里我們就理解為創建了一個ref
。
緊接着inputRef
與函數組件內部的input
相關聯,也就是說現在函數組件內是可以直接使用input
內置的屬性方法。前面說了,函數組件自己內部還是可以使用ref
的,只是我們不能直接用ref
關聯一個函數組件,但是前面我們通過forwardRef
給函數組件升了級。
緊接着,我們通過useImperativeHandle
將函數組件內部能訪問到的input
上的屬性方法,再次暴露給了父組件,useImperativeHandle
中的ref
其實就是上層傳遞的,這里就是通過此方法,將上層ref
與函數組件內部產生了關聯。我們自己定義了一個focus
方法,而這個方法內部執行的卻是input
的focus
方法,組件內部可以通過inputRef.current.focus
訪問到input
的方法(希望你沒繞暈。)

於是我們在父組件中通過this.echoRef.current.focus()
訪問到了函數組件暴露給它的方法,而這個方法本質執行的其實是input
自帶的focus
方法。
不知道你理解沒有,這里我們通過forwardRef
幫父組件做了一次轉發,父組件其實想訪問的就是input
的方法,但是函數組件在中間隔了一層,父組件就沒法直接拿到,而我們通過useImperativeHandle
幫父組件代勞了一次,成功達到了目的。
其實forwardRef
除了能讓函數組件使用ref
外,還有另一種強大的作用就是轉發ref
。
比如A>B>C
的組件結構,你在A中創建了一個ref
,你希望將這個ref
作為props
傳遞給B,然后在B中接受這個ref
再去去關聯C,以達到在A中可以訪問到C的屬性。我們可以假想有一個hoc
的場景,父組件希望訪問B組件,但是B組件被hoc
包裹了一層,也就是一個高階組件。此時你的ref
假設綁定在了hoc
生成的B,那么ref
將訪問hoc
組件而非B組件。那么怎么讓父組件可以訪問到這個B組件呢?我們可以借用forwardRef
:
import React, { Component, forwardRef } from 'react';
import ReactDOM from 'react-dom';
function hocComponent(Component) {
// 單純包裝了傳入的組件,生成了一個新的組件,只是在生成中我們還用了forwardRef在外面包了一層
return forwardRef((props, ref) => {
return <Component {...props} ref={ref} />
})
}
class Parent extends Component {
constructor(props) {
super(props);
// 創建一個ref,這個ref隨便你取什么名字
this.echoRef = React.createRef();
}
componentDidMount() {
console.log(this.echoRef);
this.echoRef.current.handleClick();
}
render() {
// 傳入Children給高階組件,得到了一個新組件
const Child = hocComponent(Children);
return (
<Child ref={this.echoRef} />
)
}
}
class Children extends Component {
constructor(props) {
super(props);
}
handleClick = () => {
console.log('給父級調用的方法')
}
render() {
return <>我是子組件啊</>;
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);

這個例子就解釋了hoc
的情況,這個例子相對上面參雜了hook
的例子來說應該好理解一點,這里就不多解釋。至少到這里,我們解釋了forwardRef
的兩種作用,一是也可以給函數組件綁定ref
,第二點就是ref
轉發,比如hoc
包裹,我們繞過繞過高階組件,拿到高階組件內部真正的組件屬性。
另外!!!A>B>C
,假設B是函數組件,我們希望A的ref
綁定C從而訪問C,其實還有一種做法,就是不要直接ref
綁定,而是把ref
作為props
傳遞下去后再綁定,這樣不管B是不是函數組件,都能成功綁定到C,再來個例子:
import React, { Component, forwardRef } from 'react';
import ReactDOM from 'react-dom';
function Children(props) {
return (
// 子組件接受了這個ref,然后再通過ref進行綁定
<input ref={props.inputRef} />
);
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.echoRef = React.createRef();
}
componentDidMount() {
console.dir(this.echoRef.current);
}
handleClick = () => {
// 成功訪問了子組件下的子組件
this.echoRef.current.focus();
}
render() {
return (
<>
<Children
inputRef={this.echoRef}//我們希望把這個ref作為props傳遞下去
/>
<button onClick={this.handleClick}>點我聚焦</button>
</>
);
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
因為給函數組件上添加ref
會警告,那么我們就不用ref
,而是把創建的ref
作為屬性傳下去,在子組件中接受后,再綁定給你要訪問的DOM或者組件,這樣不僅解決了函數組件綁定ref
的問題,還搞定了ref
轉發的問題,一箭雙雕。
叄 ❀ 回調模式與字符串模式
上面我們介紹了createRef
創建ref
的模式,接下來補充函數回調模式與字符串模式,因為用法介紹的比較多了,這里只是介紹寫法。
叄 ❀ 壹 回調模式
在第二小節中,我們通過createRef()
模式介紹了ref
的基本用法與部分使用場景,其實react
還支持函數回調的形式來綁定ref
,這種模式下不需要借用createRef
創建一個ref
,而是直接將需要綁定的DOM或者組件傳遞到函數中進行關聯,直接看個例子:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.inputRef);
}
setRef = (e) => {
this.inputRef = e;
}
// 此方法暴露給父組件
inputFocus = () => {
this.inputRef.focus();
}
render() {
return <input ref={this.setRef} />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
// 拿到子組件內定義的方法
this.echoRef.inputFocus();
}
// 定義綁定ref函數
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 這里不再直接綁定ref,而是上述函數
<Children ref={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
在上述代碼中我們並沒使用API創建ref
,而是直接定義了一個函數,此函數接受的參數其實就是你所綁定此方法的DOM或者組件,比如在父組件中,我們通過ref={this.setRef}
將Children
與父組件中的this.echoRef
進行了綁定。而在子組件中,我們同樣通過此方法綁定了input
,最終我們在父組件中通過this.echoRef.inputFocus
間接調用了input
的方法。
在父子組件的componentDidMount
中我們添加了輸出信息,打開控制台可以看到父組件成功訪問到了子組件,而子組件成功訪問到它所綁定的input
。

上述代碼中,我們還是通過ref={callback}
的形式進行子組件綁定,另外callback
也能作為props
傳遞后,再在子組件中進行綁定,比如:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
// 子組件接受傳遞的回調進行綁定
props.setRef(this);
}
componentDidMount() {
console.dir(this.inputRef);
}
setRef = (e) => {
this.inputRef = e;
}
// 此方法暴露給父組件
inputFocus = () => {
this.inputRef.focus();
}
render() {
return <input ref={this.setRef} />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
// 拿到子組件內定義的方法
this.echoRef.inputFocus();
}
// 定義綁定ref函數
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 這里直接把回調傳遞下去了
<Children setRef={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
在子組件的constructor
中我們通過props.setRef(this)
這一句,拿到父組件傳遞的callback
然后綁定了當前實例,從而達到目的。還記得前面提到的A>B>C
場景嗎,回調模式也能這么玩,再來個例子:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
}
render() {
// 父級傳遞來的props,最后給了input用了
return <input ref={this.props.setRef} />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
// 拿到子組件內定義的方法
this.echoRef.focus();
}
// 定義綁定ref函數
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 這里直接把回調傳遞下去了
<Children setRef={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);
關於回調模式就說到這里,我們最后來說下字符串模式。
叄 ❀ 貳 字符串模式
由於字符串模式在react
官方文檔中已明確廢棄,未來可能會移除,所以這里只給一個例子,在日常開發中不推薦這種做法進行ref
綁定:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
sayName=()=>{}
render() {
return <input />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.refs);
// 拿到子組件內定義的方法
// this.echoRef.focus();
}
render() {
return (
// 這里直接把回調傳遞下去了
<>
<Children ref='echoRef' />
<input ref='timeRef' />
</>
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);

在上述代碼中,我們通過ref='refName'
的形式為DOM
或者組件綁定ref
,同樣,我們可以通過this.refs.refName
拿到對應綁定的組件,其實看到refs
這個負數,你就應該可以猜到它能訪問當前組件實例中的所有ref
,上面的例子也驗證了這一點,關於字符串形式就聊這么多。
肆 ❀ 總
OK,讓我們回顧本文所提到的知識點,介紹了三種ref方式,React.createRef
,函數回調以及字符串模式,當然我們不推薦字符串,因為它已被官方廢棄。
函數組件不支持為其添加ref
屬性,因為函數組件沒有實例,但我們提到可以通過forwardRef
對於函數組件進行包裹,畢竟對於hook
而言,組件都是函數組件,react
也是提供了此做法來解決ref
獲取的問題。
我們介紹了A>B>C
的組件嵌套場景,如何將A中定義的ref
轉發給C綁定了,除了forwardRef
做法外,我們知道ref
也能定義好后,作為props
向下傳遞。
另外,我們在通過ref
獲取子組件屬性時,比如獲取一個函數,請注意函數的寫法,比如這個例子中,我們能拿到sayName
,但拿不到sayAge
,這是因為后者本質上是綁定在原型上,無法通過這種方式直接訪問,但是你可以通過原型找到它。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Children extends Component {
constructor(props) {
super(props);
}
sayName = () => { }
sayAge() { }
render() {
// 父級傳遞來的props,最后給了input用了
return <input />;
}
}
class Parent extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
console.dir(this.echoRef);
}
// 定義綁定ref函數
setRef = (e) => {
this.echoRef = e;
}
render() {
return (
// 這里直接把回調傳遞下去了
<Children ref={this.setRef} />
)
}
}
ReactDOM.render(
<Parent />,
document.getElementById('root')
);

要介紹的大概就這么多了,那么本文結束。