React專題:操作DOM

馬蹄疾發表於2018-09-06

本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出

來我的 GitHub repo 閱讀完整的專題文章

來我的 個人部落格 獲得無與倫比的閱讀體驗

React存在的意義就是狀態與UI分離,使開發者不知有DOM,無論魏晉。

不過有些狀態是無法與UI分離的,比如說表單的聚焦,複雜的動畫等等。

怎麼辦?在React完全控制DOM之前,它還是給開發者留了後門。

this.refs

?這是React不再推薦使用的API。

每一個class元件例項化的時候都會掛載一個refs屬性,它就是用來儲存DOM引用的。

在DOM元素上傳入一個值是字串的ref屬性,開發者就獲得了該DOM元素的引用,我們可以在this.refs物件下面找到它。

既然是獲取DOM元素的引用,那肯定要等元件掛載完成才能操作它。

不過React已經不推薦這種寫法。主要是它比較耗效能,因為UI會經歷很多次更新,而字串引用無法自動跟蹤DOM的變化,React要做一些額外的處理。

也許將來某個版本我們就看不到例項的refs屬性了。

import React, { Component } from 'react';

class App extends Component {
    componentDidMount() {
        this.refs.textInput.focus();
    }
    
    render() {
        return (
            <input type="text" ref="textInput" />
        );
    }
}

export default App;
複製程式碼

callback

React還支援用一個回撥接收DOM元素的引用。

但是記住,回撥不可以寫成el => this.refs.textInput = el,因為this.refs是不可以直接進行寫操作的。

import React, { Component } from 'react';

class App extends Component {
    componentDidMount() {
        this.textInput.focus();
    }
    
    render() {
        return (
            <input type="text" ref={el => this.textInput = el} />
        );
    }
}

export default App;
複製程式碼

當然,回撥神通廣大,比如說,它會穿牆術。

依然是使用回撥接收DOM元素的引用,不過這次的回撥是父元件通過props傳下來的。

一旦子元件掛載完成,就會執行ref回撥,父元件就得到子元件某個DOM元素的引用了。

import React, { Component } from 'react';
import Search from './Search';

class App extends Component {
    getInputRef = (ref) => {
        this.node = ref;
    }
    
    render() {
        return (
            <Search ref={this.getInputRef} />
        );
    }
}

export default App;
複製程式碼
import React from 'react';

const Search = (props) => (
    <input type="text" ref={props.getInputRef} />
);

export default Search;
複製程式碼

createRef

?這是React v16.3.0釋出的API。

createRef的作用就是建立一個ref物件。

先把createRef的執行結果返回給一個例項屬性,然後通過該例項屬性獲得DOM元素的引用。

注意事項:

  • createRef初始化動作要在元件掛載之前,如果是掛載之後初始化,則無法得到DOM元素的引用。
  • 真正的DOM元素引用在current屬性上。
import React, { Component, createRef } from 'react';

class App extends Component {
    textInput = createRef();

    componentDidMount() {
        this.textInput.current.focus();
    }

    render() {
        return (
            <input type="text" ref={this.textInput} />
        );
    }
}

export default App;
複製程式碼

出於不可描述的原因,如果你想獲取一個子元件的ref引用,那麼子元件必須是class元件。

因為你獲取的實際上是子元件的例項,而函式式元件是沒有例項的。

所有獲取ref引用的方式,如果想要獲取子元件而不是DOM元素,子元件都不能是函式式元件。

import React, { Component, createRef } from 'react';
import Child from './Child';

class App extends Component {
    childRef = createRef();

    render() {
        return (
            <Child ref={this.childRef} />
        );
    }
}

export default App;
複製程式碼

forwardRef

?這是React v16.3.0釋出的API。

使用回撥可以獲取子元件的DOM元素引用,不過這種技巧終究是hack。

所以貼心的React為我們提供了一個會穿牆術的武林正派。

父元件的寫法並沒有什麼特別,只不過這次createRef返回的結果不是傳給自己的某個DOM元素,而是子元件。

關鍵在於子元件,子元件把自己整個作為引數傳給了forwardRef,然後子元件就在props引數之外,獲得了ref引數,再把ref引數賦值給某個DOM元素的ref屬性。

發現了嗎?forwardRef充當的是一個傳遞者的角色,它實際上是一個容器元件。

向前傳遞,這就是叫forwardRef的原因。

需要特別注意,使用forwardRef時,該元件必須是函式式元件。原因可能是React不想破壞class元件的引數體系。

誒,前面不是說了獲取元件的ref引用時不能使用函式式元件麼?

仔細看,兩者是有本質區別的,這裡獲取的依然是DOM元素,只不過跨級了。

import React, { Component, createRef } from 'react';
import Search from './Search';

class App extends Component {
    textInput = createRef();

    componentDidMount() {
        this.textInput.current.focus();
    }

    render() {
        return (
            <Search ref={this.textInput} />
        );
    }
}

export default App;
複製程式碼
import React, { forwardRef } from 'react';

const Search = forwardRef((props, ref) => (
    <input type="text" ref={ref} />
));

export default Search;
複製程式碼

既然跨級,能不能玩大點?

當然可以。

事實上,一旦被forwardRef包裹的子元件接收到了ref引數,它可以繼續將ref往下傳遞。通過什麼傳遞,當然是props啦!

之後ref就變成了一個普通的props,任你差遣,直到它被掛載到某個DOM元素的ref屬性上。

發現沒有,再往下就不區分class元件和函式式元件了,因為class元件和函式式元件都可以接收props。它的任務只是幫某個不知道多少代的祖先把這個特定的prop掛載到特定的DOM元素上。

其實ref回撥也是可以跨多級傳遞的,原理同上。

import React, { Component, createRef } from 'react';
import Search from './Search';

class App extends Component {
    textInput = createRef();

    render() {
        return (
            <Search ref={this.textInput} />
        );
    }
}

export default App;
複製程式碼
import React from 'react';
import Input from './Input';

const Search = forwardRef((props, ref) => (
    <Input inputRef={ref} />
));

export default Search;
複製程式碼
import React, { Component } from 'react';

class Input extends Component {
    render() {
        return (
            <input type="text" ref={this.props.inputRef} />
        );
    }
}

export default Input;
複製程式碼

React專題一覽

什麼是UI

JSX

可變狀態

不可變屬性

生命週期

元件

事件

操作DOM

抽象UI

相關文章