《深入 React 技術?!氛鹿澰囎x

轉載自:前端外刊評論-知乎專欄——《深入 React 技術棧》章節試讀

作者: [流形](https://www.zhihu.com/people/arcthur/answers)

《深入 React 技術棧》盡管并不是一本外文譯作,但 React 與 Redux 方面的經驗都是從國外的著作、博客與文檔中汲取,結合對它們的理解與實踐沉淀在 pure render 專欄上,所謂知識無國界,前端外刊評論亦是如此。@寸志 兄在本書寫作期間參與了試讀,并是本書的推薦人之一,在此非常感謝。 — 陳屹 @流形

1.6 React 與 DOM

前面已經介紹完組件的組成部分了,但還缺少最后一環,那就是將組件渲染到真實 DOM 上。從 React 0.14 版本開始,React 將 React 中涉及 DOM 操作的部分剝離開,目的是為了抽象 React,同時適用于 Web 端和移動端。ReactDOM 的關注點在 DOM 上,因此只適用于 Web 端。

在 React 組件的開發實現中,我們并不會用到 ReactDOM,只有在頂層組件以及由于 React 模型所限而不得不操作 DOM 的時候,才會使用它。

1.6.1 ReactDOM

ReactDOM 中的 API 非常少,只有 findDOMNode、unmountComponentAtNoderender,我們就以 API 的角度來講講它們的用法。

findDOMNode
上一節我們已經講過組件的生命周期,DOM 真正被添加到 HTML 中的生命周期方法是 componentDidMountcomponentDidUpdate 方法。在這兩個方法中,我們可以獲取真正的 DOM 元素。React 提供的獲取 DOM 元素的方法有兩種,其中一種就是 ReactDOM 提供的 findDOMNode。

DOMElement findDOMNode(ReactComponent component)

當組件被渲染到 DOM 中后,findDOMNode 返回該 React 組件實例相應的 DOM 節點。它可以被用于獲取表單的 value 以及 DOM 的測量上。例如,假設要在當前組件加載完時獲取當前 DOM,就可以使用 findDOMNode

import React, { Component } from 'react';

class App extends Component {
    componentDidMount() { // this 為當前組件的實例 
        const dom = findDOMNode(this);
    } 
    render() {}
}

如果在 render 中返回 null,那么 findDOMNode 也返回 null。findDOMNode 只對已經掛載的組件有效。

涉及到復雜操作時,還有非常多的原生 DOM API 可以用。但是需要嚴格限制場景,在使用之前多問自己為什么要操作 DOM。

render
為什么說只有在頂層組件我們不得不使用 ReactDOM,因為我們要把 React 渲染的 Virtual DOM 渲染到瀏覽器的 DOM 當中,這就要使用 render 方法了。

ReactComponent render( ReactElement element, DOMElement container, [function callback])

render 方法把元素掛載到 container 中,并且返回 element 的實例(即 ref 引用)。當然,如果是無狀態組件,render 會返回 null。當組件裝載完畢時,callback 就會被調用。

當組件在初次渲染之后再次更新時,React 不會把整個組件重新渲染一次,而會用它高效的 DOM diff 算法做局部的更新。這也是 React 最大的亮點之一!

此外,與 render 相反,React 還提供了一個很少使用 unmountComponentAtNode 方法來做 unMount 的操作。

1.6.2 ReactDOM 不穩定方法

ReactDOM 中有兩個不穩定方法,其中一個方法與 render 方法頗為相似。講起它,還得從我們常用的 Dialog 組件在 React 中的實現講起。

我們先來回憶一下 Dialog 組件的特點,它是不在文檔流中的彈出框,一般會絕對定位在屏幕的正中央,背后有一層半透明的遮罩。因此,它往往直接渲染在 document.body 下,然而我們并不知道如何在 React 組件外進行操作。這就要從實現 Dialog 的思路以及涉及 DOM 部分的實現講起。

這里我們引入 Portal 組件,這是一個經典的實現,最初的實現來源于 React Bootstrap 組件庫中的 Overlay Mixin,后來使用越來越廣泛。我們截取關鍵部分的源碼:

import React from 'react';
import ReactDOM, {findDOMNode} from 'react-dom';
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations';

export default class Portal extends React.Component {
    constructor() { // ... } 
    openPortal(props = this.props) {
        this.setState({
            active: true
        });
        this.renderPortal(props);
        this.props.onOpen(this.node);
    }
    closePortal(isUnmounted = false) {
        const resetPortalState = () => {
            if (this.node) {
                ReactDOM.unmountComponentAtNode(this.node);
                document.body.removeChild(this.node);
            }
            this.portal = null;
            this.node = null;
            if (isUnmounted !== true) {
                this.setState({
                    active: false
                });
            }
        };
        if (this.state.active) {
            if (this.props.beforeClose) {
                this.props.beforeClose(this.node, resetPortalState);
            } else {
                resetPortalState();
            }
            this.props.onClose();
        }
    }
    renderPortal(props) {
        if (!this.node) {
            this.node = document.createElement('div'); // 在節點增加到 DOM 之前,執行 CSS 防止無效的重繪 
            this.applyClassNameAndStyle(props); 
            document.body.appendChild(this.node); 
        } else { // 當新的 props 傳下來的時候,更新 CSS 
            this.applyClassNameAndStyle(props); 
        } 
        let children = props.children; // https://gist.github.com/jimfb/d99e0678e9da715ccf6454961ef04d1b 
        if (typeof props.children.type === 'function') { 
            children = React.cloneElement(props.children, { 
                closePortal: this.closePortal 
            }); 
        } 
        this.portal = ReactDOM.unstable_renderSubtreeIntoContainer( this, children, this.node, this.props.onUpdate ); 
    } 
    render() { 
        if (this.props.openByClickOn) { 
            return React.cloneElement(
                this.props.openByClickOn, { onClick: this.handleWrapperClick }); 
        } 
        return null; 
    }
}

Portal 組件看得出,我們實現了一個『殼』,包括觸發事件,渲染的位置以及暴露的方法,它并不關心子組件的內容。當我們使用它的時候,可以這么寫。

<Portal ref="myPortal"> 
    <Modal title="My modal"> Modal content </Modal>
</Portal>

這個組件可以說是 Dialog 實現的精髓,我們 Dialog 的行為抽象了 Portal 這個父組件。

當我們調用上述代碼時,可以注意到在 componentDidMount 的時候最后調用了 this.renderPortal() 方法,這個方法把 children 里的內容插入到 document.body 下,這就實現 children 不在標準文檔流的渲染。

這之間就說到了 ReactDOM 中不穩定的 API 方法 unstable_renderSubtreeIntoContainer 。它的作用很簡單,就是更新組件到傳入的 DOM 節點上,我們在這里使用它完成了在組件內實現跨組件的 DOM 操作。

這個方法與 render 是不是很相似,但 render 方法缺少了一個插件某一個節點的參數。從最終 ReactDOM 方法實現的源代碼 react/src/renderers/dom/client/ReactMount.js 中了解 unstable_renderSubtreeIntoContainerrender 方法對應調用的方法區別是:

  • render: ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);

  • unstable_renderSubtreeIntoContainer: ReactMount._renderSubtreeIntoContainer(parentComponent, nextElement, container, callback);

源代碼證明了我們的猜想,也就說明兩者區別在于是否傳入父節點。

另一個不穩定方法 unstable_renderSubtreeIntoContainer 關于 setState 的更新策略,我們會在『$3.4 解密 setState』中詳細介紹。

1.6.3 refs

剛才我們已經詳述了 ReactDOM 的 render 方法,比如我們渲染了一個 App 組件到 root節點下了:

const myAppInstance = ReactDOM.render(<App />,
document.getElementById('root'));
myAppInstance.doSth();

我們利用 render 方法拿到了 App 組件的實例,然后就可以對它做一些操作。但在組件內,JSX 是不會返回一個組件的實例的!它只是一個 ReactElement,只是告訴 React 被掛載的組件應該長什么樣。

const myApp = <App />;

refs 就是為此而生的,它是 React 組件中非常特殊的 prop,可以附加到任何一個組件上。從字面意思來看,efsreference,組件被調用時會新建一個該組件的實例,而 refs 就會指向這個實例。

它可以是一個回調函數,這個回調函數會在組件被掛載后立即執行。例如:

import React, { Component } from 'react';

class App extends Component {
    constructor(props){
        super(props); 
        this.handleClick = this.handleClick.bind(this); 
    } 
    handleClick() { 
        if (this.myTextInput !== null) { 
            this.myTextInput.focus(); 
        } 
    } 
    render() { 
    return ( 
        <div> 
            <input type="text" ref={(ref) => this.myTextInput = ref} />
            <input type="button" value="Focus the text input" onClick={this.handleClick} />
        </div> ); 
    }
}

在這個例子里,我們拿到 input 的真正實例,所以我們可以在按鈕被按下后調用輸入框的 focus() 方法。這個例子把 refs 放到原生的 DOM 組件 input 中,我們可以通過 refs 得到 DOM 節點;而如果把 refs 放到 React 組件,比如 <TextInput />,我們獲得的就是 TextInput 的實例,因此我們可以調用 TextInput 的實例方法。

refs 同樣支持字符串。對于 DOM 操作,我們不僅可以使用 findDOMNode 獲得該組件 DOM,還可以使用 ref 獲得組件內部的 DOM。比如:

import React, { Component } from 'react';

class App extends Component {
     componentDidMount() { // myComp 是 Comp 的一個實例,因此需要用 findDOMNode 轉換為相應的 DOM 
        const myComp = this.refs.myComp;
        const dom = findDOMNode(myComp); 
    } 
    render() { 
        return ( 
            <div> 
                <Comp ref="myComp" /> 
            </div> ); 
    }
}

要獲取一個 React 組件的引用,既可以使用 this 來獲取當前 React 組件,也可以使用 refs 來獲取你擁有的子組件的引用。

我們回到 1.6.2 節中 Portal 組件里暴露的兩個方法 openPortalclosePortal。這兩個方法的調用方式為:

this.refs.myPortal.openPortal();
this.refs.myPortal.closePortal();

這種命令式調用的方式,盡管說并不是 React 推崇的,但我們仍然可以使用。這里我們原則上在組件狀態維護上不建議用這種方式。

為了防止內存泄露,當一個組件卸載的時候,組件里所有的 ref 就會變為 null。
值得注意的是,findDOMNoderefs 都無法用于無狀態組件中,原因在前面已經說過。無狀態組件掛載時只是方法調用,沒有新建實例。

對于 React 組件來說,refs 會指向一個組件類的實例,所以可以調用該類定義的任何方法。如果需要訪問該組件的真實 DOM,可以用 ReactDOM.findDOMNode 來找到 DOM 節點,但我們并不推薦這樣做。因為這在大部分情況下都打破了封裝性,而且通常都能用更清晰的辦法在 React 中構建代碼。

1.6.4 React 之外的 DOM 操作

DOM 操作可以歸納為對 DOM 的增刪改查。這里的『查』指的是對 DOM 屬性、樣式的查看,比如查看 DOM 的位置、寬高信息。而要對 DOM 進行增刪改查,就要先 query 到 DOM。

React 的聲明式渲染機制,把復雜的 DOM 操作抽象為簡單的 stateprops 的操作,因此避免了很多直接的 DOM 操作。不過,仍然有一些 DOM 操作是 React 無法避免或者正在努力避免的。

舉一個明顯的例子,如果要調用 HTML5 Audio/Video 的 play 方法,input 的 focus 方法,React 就無能為力了。這時我們只能使用相應的 DOM 方法來實現。

React 提供了事件綁定的功能,但是仍然有一些特殊情況需要自行綁定事件。例如 Popup 等類似組件,當點擊組件其它區域可以收縮此類組件。這就要求我們對組件以外的區域(一般指 document、body)進行事件綁定。例如:

componentDidUpdate(prevProps, prevState) {
    if (!this.state.isActive && prevState.isActive) {
        document.removeEventListener('click', this.hidePopup);
    }
    if (this.state.isActive && !prevState.isActive) {
        document.addEventListener('click', this.hidePopup);
    }
}
componentWillUnmount() {
    document.removeEventListener('click', this.hidePopup);
}
hidePopup(e) {
    if (!this.isMounted()) {
        return false;
    }
    const node = ReactDOM.findDOMNode(this);
    const target = e.target || e.srcElement;
    const isInside = node.contains(target);
    if (this.state.isActive && !isInside) {
        this.setState({
            isActive: false,
        });
    }
}

React 中使用 DOM 最多的還是計算 DOM 的尺寸(即位置信息)。我們可以提供像 width,或 height 這樣的工具函數:

function width(el) {
    const styles = el.ownerDocument.defaultView.getComputedStyle(el, null);
    const width = parseFloat(styles.width.indexOf('px') !== -1 ? styles.width : 0);
    const boxSizing = styles.boxSizing || 'content-box';
    if (boxSizing === 'border-box') {
        return width;
    }
    const borderLeftWidth = parseFloat(styles.borderLeftWidth);
    const borderRightWidth = parseFloat(styles.borderRightWidth);
    const paddingLeft = parseFloat(styles.paddingLeft);
    const paddingRight = parseFloat(styles.paddingRight);
    return width - borderRightWidth - borderLeftWidth - paddingLeft - paddingRight;
}

但上述計算方法并不能完全覆蓋所有情況,這需要付出不少的成本去實現。值得高興的是,React 正在自己構建一個 DOM 排列模型,來努力避免這些 React 之外的 DOM 操作。我們相信在不久的將來,React 的使用者就可以完全拋棄掉 jQuery 等 DOM 操作庫。
可以說在 React 組件開發中,還有很多意料之外的情形。在這些情形中,應該如何運用 React 的方式優雅地解決問題是我們需要一直思考的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,428評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,024評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,285評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,548評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,328評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,878評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,971評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,098評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,616評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,554評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,725評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,243評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,971評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,361評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,613評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,339評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,695評論 2 370

推薦閱讀更多精彩內容

  • 原教程內容詳見精益 React 學習指南,這只是我在學習過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,849評論 1 18
  • It's a common pattern in React to wrap a component in an ...
    jplyue閱讀 3,289評論 0 2
  • 第一章(Raact數據流、React生命周期、React與DOM) React數據流 在React中,數據是自項向...
    吳林霏smile閱讀 393評論 0 1
  • react 基本概念解析 react 的組件聲明周期 react 高階組件,context, redux 等高級...
    南航閱讀 1,075評論 0 1
  • 南云國都,飛雪別院的靜室內。 白衣少年、金衣少年都盤膝而坐,一心修行著,這已經是他從樊氏魔山回來的三年后了。 當然...
    im喵小姐閱讀 848評論 0 0