從零開始的react入門教程(十一),react ref 詳解,三種寫法與 ref 轉發(傳遞)


壹 ❀ 引

在前面的文章中,我們介紹了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.echoRefdiv元素。所以可以得知,通過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或者微信小程序,reactFragment對標vuetemplate與小程序的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與refprops很好理解,就是上層傳遞下來的屬性,而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方法,而這個方法內部執行的卻是inputfocus方法,組件內部可以通過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')
);

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

伍 ❀ 參考

Refs and the DOM

Refs 轉發

React.forwardRef真是個好東西

你不知道的 useRef


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM