《深入 React 技術棧》章節試讀

《深入 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`、`unmountComponentAtNode` 和 `render`,我們就以 API 的角度來講講它們的用法。

findDOMNode

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

DOMElement findDOMNode(ReactComponent component)n

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

import React, { Component } from react;nnclass App extends Component {n componentDidMount() {n // this 為當前組件的實例n const dom = findDOMNode(this);n }nn render() {}n}n

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

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

render

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

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

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;nimport ReactDOM, { findDOMNode } from react-dom;nimport CSSPropertyOperations from react/lib/CSSPropertyOperations;nnexport default class Portal extends React.Component {n constructor() {n // ...n }nn openPortal(props = this.props) {n this.setState({ active: true });n this.renderPortal(props);n this.props.onOpen(this.node);n }nn closePortal(isUnmounted = false) {n const resetPortalState = () => {n if (this.node) {n ReactDOM.unmountComponentAtNode(this.node);n document.body.removeChild(this.node);n }n this.portal = null;n this.node = null;n if (isUnmounted !== true) {n this.setState({ active: false });n }n };nn if (this.state.active) {n if (this.props.beforeClose) {n this.props.beforeClose(this.node, resetPortalState);n } else {n resetPortalState();n }nn this.props.onClose();n }n }nnn renderPortal(props) {n if (!this.node) {n this.node = document.createElement(div);n // 在節點增加到 DOM 之前,執行 CSS 防止無效的重繪n this.applyClassNameAndStyle(props);n document.body.appendChild(this.node);n } else {n // 當新的 props 傳下來的時候,更新 CSSn this.applyClassNameAndStyle(props);n }nn let children = props.children;n // https://gist.github.com/jimfb/d99e0678e9da715ccf6454961ef04d1bn if (typeof props.children.type === function) {n children = React.cloneElement(props.children, { closePortal: this.closePortal });n }nn this.portal = ReactDOM.unstable_renderSubtreeIntoContainer(n this,n children,n this.node,n this.props.onUpdaten );n }nn render() {n if (this.props.openByClickOn) {n return React.cloneElement(this.props.openByClickOn, { onClick: this.handleWrapperClick });n }n return null;n }n}n

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

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

這個組件可以說是 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_renderSubtreeIntoContainer 與 render 方法對應調用的方法區別是:

  • 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));nmyAppInstance.doSth();n

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

const myApp = <App />;n

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

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

import React, { Component } from react;nnclass App extends Component {n constructor(props){n super(props);nn this.handleClick = this.handleClick.bind(this);n }nn handleClick() {n if (this.myTextInput !== null) {n this.myTextInput.focus();n }n }nn render() {n return (n <div>n <input type="text" ref={(ref) => this.myTextInput = ref} />n <inputn type="button"n value="Focus the text input"n onClick={this.handleClick}n />n </div>n );n }n}n

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

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

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

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

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

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

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

為了防止內存泄露,當一個組件卸載的時候,組件里所有的 ref 就會變為 null。

值得注意的是,findDOMNode 和 refs 都無法用於無狀態組件中,原因在前面已經說過。無狀態組件掛載時只是方法調用,沒有新建實例。

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

1.6.4 React 之外的 DOM 操作

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

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

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

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

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

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

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

但上述計算方法並不能完全覆蓋所有情況,這需要付出不少的成本去實現。值得高興的是,React 正在自己構建一個 DOM 排列模型,來努力避免這些 React 之外的 DOM 操作。我們相信在不久的將來,React 的使用者就可以完全拋棄掉 jQuery 等 DOM 操作庫。

可以說在 React 組件開發中,還有很多意料之外的情形。在這些情形中,應該如何運用 React 的方式優雅地解決問題是我們需要一直思考的。

推薦閱讀:

用 Three.js, React 和 WebGL 開發遊戲 — SitePoint
React Loadable 簡介
掌握這5大核心概念,你就理解了React
React 模態框秘密和「輪子」漸進設計

TAG:前端开发 | React | 前端框架 |