《深入 React 技術椥笛玻》章節(jié)試讀

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

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

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

1.6 React 與 DOM

前面已經(jīng)介紹完組件的組成部分了趋急,但還缺少最后一環(huán),那就是將組件渲染到真實 DOM 上势誊。從 React 0.14 版本開始呜达,React 將 React 中涉及 DOM 操作的部分剝離開,目的是為了抽象 React粟耻,同時適用于 Web 端和移動端查近。ReactDOM 的關注點在 DOM 上,因此只適用于 Web 端挤忙。

在 React 組件的開發(fā)實現(xiàn)中霜威,我們并不會用到 ReactDOM,只有在頂層組件以及由于 React 模型所限而不得不操作 DOM 的時候册烈,才會使用它戈泼。

1.6.1 ReactDOM

ReactDOM 中的 API 非常少,只有 findDOMNodeunmountComponentAtNoderender大猛,我們就以 API 的角度來講講它們的用法扭倾。

findDOMNode
上一節(jié)我們已經(jīng)講過組件的生命周期,DOM 真正被添加到 HTML 中的生命周期方法是 componentDidMountcomponentDidUpdate 方法挽绩。在這兩個方法中膛壹,我們可以獲取真正的 DOM 元素。React 提供的獲取 DOM 元素的方法有兩種唉堪,其中一種就是 ReactDOM 提供的 findDOMNode模聋。

DOMElement findDOMNode(ReactComponent component)

當組件被渲染到 DOM 中后,findDOMNode 返回該 React 組件實例相應的 DOM 節(jié)點巨坊。它可以被用于獲取表單的 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 只對已經(jīng)掛載的組件有效占调。

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

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

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

render 方法把元素掛載到 container 中攻人,并且返回 element 的實例(即 ref 引用)取试。當然,如果是無狀態(tài)組件怀吻,render 會返回 null瞬浓。當組件裝載完畢時,callback 就會被調(diào)用蓬坡。

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

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

1.6.2 ReactDOM 不穩(wěn)定方法

ReactDOM 中有兩個不穩(wěn)定方法,其中一個方法與 render 方法頗為相似。講起它掂林,還得從我們常用的 Dialog 組件在 React 中的實現(xiàn)講起臣缀。

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

這里我們引入 Portal 組件赖阻,這是一個經(jīng)典的實現(xiàn),最初的實現(xiàn)來源于 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'); // 在節(jié)點增加到 DOM 之前,執(zhí)行 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 組件看得出苇侵,我們實現(xiàn)了一個『殼』,包括觸發(fā)事件企锌,渲染的位置以及暴露的方法榆浓,它并不關心子組件的內(nèi)容。當我們使用它的時候撕攒,可以這么寫陡鹃。

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

這個組件可以說是 Dialog 實現(xiàn)的精髓,我們 Dialog 的行為抽象了 Portal 這個父組件抖坪。

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

這之間就說到了 ReactDOM 中不穩(wěn)定的 API 方法 unstable_renderSubtreeIntoContainer 。它的作用很簡單捌肴,就是更新組件到傳入的 DOM 節(jié)點上蹬叭,我們在這里使用它完成了在組件內(nèi)實現(xiàn)跨組件的 DOM 操作。

這個方法與 render 是不是很相似状知,但 render 方法缺少了一個插件某一個節(jié)點的參數(shù)秽五。從最終 ReactDOM 方法實現(xiàn)的源代碼 react/src/renderers/dom/client/ReactMount.js 中了解 unstable_renderSubtreeIntoContainerrender 方法對應調(diào)用的方法區(qū)別是:

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

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

源代碼證明了我們的猜想,也就說明兩者區(qū)別在于是否傳入父節(jié)點饥悴。

另一個不穩(wěn)定方法 unstable_renderSubtreeIntoContainer 關于 setState 的更新策略坦喘,我們會在『$3.4 解密 setState』中詳細介紹盲再。

1.6.3 refs

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

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

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

const myApp = <App />;

refs 就是為此而生的蓖救,它是 React 組件中非常特殊的 prop洪规,可以附加到任何一個組件上。從字面意思來看循捺,efsreference斩例,組件被調(diào)用時會新建一個該組件的實例,而 refs 就會指向這個實例从橘。

它可以是一個回調(diào)函數(shù)念赶,這個回調(diào)函數(shù)會在組件被掛載后立即執(zhí)行。例如:

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 的真正實例晶乔,所以我們可以在按鈕被按下后調(diào)用輸入框的 focus() 方法。這個例子把 refs 放到原生的 DOM 組件 input 中牺勾,我們可以通過 refs 得到 DOM 節(jié)點;而如果把 refs 放到 React 組件阵漏,比如 <TextInput />驻民,我們獲得的就是 TextInput 的實例,因此我們可以調(diào)用 TextInput 的實例方法履怯。

refs 同樣支持字符串回还。對于 DOM 操作,我們不僅可以使用 findDOMNode 獲得該組件 DOM叹洲,還可以使用 ref 獲得組件內(nèi)部的 DOM柠硕。比如:

import React, { Component } from 'react';

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

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

我們回到 1.6.2 節(jié)中 Portal 組件里暴露的兩個方法 openPortalclosePortal民泵。這兩個方法的調(diào)用方式為:

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

這種命令式調(diào)用的方式癣丧,盡管說并不是 React 推崇的,但我們?nèi)匀豢梢允褂谜蛔薄_@里我們原則上在組件狀態(tài)維護上不建議用這種方式胁编。

為了防止內(nèi)存泄露厢钧,當一個組件卸載的時候,組件里所有的 ref 就會變?yōu)?null嬉橙。
值得注意的是早直,findDOMNoderefs 都無法用于無狀態(tài)組件中,原因在前面已經(jīng)說過市框。無狀態(tài)組件掛載時只是方法調(diào)用霞扬,沒有新建實例。

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

1.6.4 React 之外的 DOM 操作

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

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

舉一個明顯的例子,如果要調(diào)用 HTML5 Audio/Video 的 play 方法习绢,input 的 focus 方法渠抹,React 就無能為力了。這時我們只能使用相應的 DOM 方法來實現(xiàn)闪萄。

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

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 這樣的工具函數(shù):

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;
}

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

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末握巢,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子松却,更是在濱河造成了極大的恐慌暴浦,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,383評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晓锻,死亡現(xiàn)場離奇詭異歌焦,居然都是意外死亡,警方通過查閱死者的電腦和手機砚哆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評論 3 385
  • 文/潘曉璐 我一進店門独撇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人躁锁,你說我怎么就攤上這事券勺。” “怎么了灿里?”我有些...
    開封第一講書人閱讀 157,852評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長程腹。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么昂灵? 我笑而不...
    開封第一講書人閱讀 56,621評論 1 284
  • 正文 為了忘掉前任琉历,我火速辦了婚禮,結(jié)果婚禮上见转,老公的妹妹穿的比我還像新娘命雀。我一直安慰自己,他們只是感情好斩箫,可當我...
    茶點故事閱讀 65,741評論 6 386
  • 文/花漫 我一把揭開白布吏砂。 她就那樣靜靜地躺著撵儿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪狐血。 梳的紋絲不亂的頭發(fā)上淀歇,一...
    開封第一講書人閱讀 49,929評論 1 290
  • 那天,我揣著相機與錄音匈织,去河邊找鬼浪默。 笑死,一個胖子當著我的面吹牛缀匕,可吹牛的內(nèi)容都是我干的纳决。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼乡小,長吁一口氣:“原來是場噩夢啊……” “哼阔加!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起劲件,我...
    開封第一講書人閱讀 37,803評論 0 268
  • 序言:老撾萬榮一對情侶失蹤掸哑,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后零远,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體苗分,經(jīng)...
    沈念sama閱讀 44,265評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,582評論 2 327
  • 正文 我和宋清朗相戀三年牵辣,在試婚紗的時候發(fā)現(xiàn)自己被綠了摔癣。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,716評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡纬向,死狀恐怖择浊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情逾条,我是刑警寧澤琢岩,帶...
    沈念sama閱讀 34,395評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站师脂,受9級特大地震影響担孔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吃警,卻給世界環(huán)境...
    茶點故事閱讀 40,039評論 3 316
  • 文/蒙蒙 一糕篇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧酌心,春花似錦拌消、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽氓英。三九已至,卻和暖如春泰鸡,著一層夾襖步出監(jiān)牢的瞬間债蓝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評論 1 266
  • 我被黑心中介騙來泰國打工盛龄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留饰迹,地道東北人。 一個月前我還...
    沈念sama閱讀 46,488評論 2 361
  • 正文 我出身青樓余舶,卻偏偏與公主長得像啊鸭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子匿值,可洞房花燭夜當晚...
    茶點故事閱讀 43,612評論 2 350

推薦閱讀更多精彩內(nèi)容

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