React躬行記(5)——React和DOM
React實現了一套與瀏覽器無關的DOM系統,包括元素渲染、節點查詢、事件處理等機制。
一、ReactDOM
自React v0.14開始,官方將與DOM相關的操作從React中剝離,組成單獨的react-dom庫,從而讓React能相容更多的終端。在引入react-dom庫後,就能呼叫一個全域性物件:ReactDOM,雖然在之前的章節中已多次使用該物件,但是都沒有給出過多的講解,本節將對其做重點分析。
ReactDOM只包含了unmountComponentAtNode()、findDOMNode()、createPortal()和render()等為數不多的幾個方法,其中在第2篇中介紹了render()的功能,又在第4篇中提到用unmountComponentAtNode()方法移除DOM中已掛載的元件。接下來會講解剩餘的兩個方法。
1)findDOMNode()
當元件被渲染到頁面真實的DOM中後,就能通過findDOMNode()方法得到生成的DOM元素,然後就能完成諸如讀值、計算尺寸等操作。
注意,findDOMNode()只能獲取已掛載的元件,並且不能用於函式元件。在元件的生命週期中,它只能存在於componentDidMount()和componentDidUpdate()兩個回撥函式中,在其它地方呼叫會丟擲一個錯誤,具體如下所示。
class Btn extends React.Component { render() { ReactDOM.findDOMNode(this); //丟擲錯誤 return <button>提交</button>; } componentDidMount() { ReactDOM.findDOMNode(this); //<button>提交</button> } }
在上面的示例中,this指向的是Btn元件例項,在將this傳給findDOMNode()方法後,得到了一個<button>元素。有一點要注意,如果元件中的render()返回null或false,那麼findDOMNode()只會返回null。
2)createPortal()
在React v16中,新增了Portal特性,能讓元件渲染到父元件以外的DOM節點中。這個特性適用於需要跳出容器的場景,例如建立頁面內定製的彈框。
在React中使用Portal特性,需要呼叫ReactDOM上的一個新方法:createPortal()。此方法能接收2個引數,第一個是可渲染的React子元素,例如字串、React元素陣列等;第二個是DOM元素,也就是要掛載的容器。關於這個方法的具體使用可參考下面的示例。
class Btn extends React.Component { render() { return ReactDOM.createPortal(this.props.children, document.body); } } ReactDOM.render(<Btn><p>按鈕</p></Btn>, document.getElementById("container"));
在上面的render()方法中呼叫了ReactDOM.createPortal(),使得<p>元素最終掛載到了<body>元素中,而不是id屬性為“container”的元素。
二、Refs
Refs是一種訪問方式,通過它可讀取render()方法內生成的元件例項和DOM元素,常用來處理元素的焦點、觸發動畫、整合第三方DOM庫等。注意,在元件的生命週期中,要讓Refs有效,得將其放在componentDidMount()和componentDidUpdate()兩個回撥函式中才行。雖然Refs能給某些場景帶來便利,但是它破壞了React通過props傳遞資料的典型資料流,因此要儘量避免使用Refs。
如果要使用Refs的功能,那麼就得設定React元素的ref屬性,它的值可以是物件、回撥函式和字串,下面會分別講解ref屬性的這三類值。
1)物件
此處的物件是React.createRef()方法的返回值,包含一個current屬性,而該屬性指向的正是要讀取的元件例項或DOM元素。下面的示例展示了ref屬性和React.createRef()方法的配合過程。
class Btn extends React.Component { constructor(props) { super(props); this.myBtn = React.createRef(); } render() { return <button ref={this.myBtn}>提交</button>; } componentDidMount() { let btn = this.myBtn.current; console.log(btn); //<button>提交</button> } }
首先在元件的建構函式中呼叫React.createRef();再將返回值賦給this.myBtn,這樣就能在元件內部的任意位置使用該物件了;然後讓this.myBtn成為<button>元素的ref屬性的值;最後在componentDidMount()中就能成功讀取到current屬性的值,從而完成了一次Refs式的訪問。
2)回撥函式
這個回撥函式能接收一個引數(如下程式碼所示),當元件被掛載時,引數的值為元件例項或DOM元素;當元件被解除安裝時,引數的值為null。
class Btn extends React.Component { render() { return ( <button ref={btn => { this.myBtn = btn }}>提交</button> ); } componentDidMount() { let btn = this.myBtn; console.log(btn); //<button>提交</button> } }
與前一種使用方式最大的不同是解除了對React.createRef()方法的依賴,在回撥函式中直接將其引數賦給this.myBtn,就能得到預期的結果,不用再呼叫一次current屬性。
3)字串
ref屬性的值還可以是字串,例如下面程式碼中的"myBtn",通過this.refs.myBtn就能訪問到想要的元件例項或DOM元素。
class Btn extends React.Component { render() { return <button ref="myBtn">提交</button>; } componentDidMount() { let btn = this.refs.myBtn; console.log(btn); //<button>提交</button> } }
不過,官方已經不推薦這種寫法了,在未來的版本中有可能會被移除,因此建議改用回撥函式的方式。
4)使用場景
ref屬性不僅能像之前示例那樣應用於DOM元素上,還能在類元件中使用ref屬性,如下程式碼所示。
class Btn extends React.Component { render() { return <button>提交</button>; } } class Container extends React.Component { render() { return <Btn ref={btn => { this.myBtn = btn }}>提交</Btn>; } componentDidMount() { let btn = this.myBtn; console.log(btn); //Btn元件的例項 } }
Container是Btn的父元件,在其render()方法中,通過回撥函式將Btn元件的例項賦給了this.myBtn。
由於函式元件沒有例項,因此不能對其新增ref屬性。
5)子元件的DOM元素
在父元件中,如果要訪問子元件的某個DOM元素,那麼單靠ref屬性是無法實現的,因為ref屬性得到的只能是子元件的例項。不過,有一種間接的方式可以實現這個需求,那就是將ref屬性和ReactDOM.findDOMNode()配合使用。下面套用上一節使用場景中的Btn和Container兩個元件,程式碼只列出了修改部分,其餘都已省略。
class Container extends React.Component { componentDidMount() { let btn = this.myBtn; let dom = ReactDOM.findDOMNode(btn); console.log(dom); //<button>提交</button> } }
在componentDidMount()方法中,呼叫了一次ReactDOM.findDOMNode(),從而得到了子元件所擁有的DOM元素。
三、Fragments
JSX結構有一個限制,那就是在最外層必須用一個元素包裹,即使這是一個冗餘的元素,也得加上。例如為一個<ul>元素掛載一組元素集合,如下所示。
class Btns extends React.Component { render() { return ( <div> <li>1</li> <li>2</li> <li>3</li> </div> ); } }
在頁面上渲染出的DOM會像下面這樣,其中<div>元素在此處是沒有作用的。
<ul id="container"> <div> <li>1</li> <li>2</li> <li>3</li> </div> </ul>
為了避免這種無意義的輸出,React引入了Fragments,其結構如下程式碼所示。只需將最外層的<div>元素的開始和結束標籤分別改成<>和</>,就不用在DOM中增加額外的元素了。
class Btns extends React.Component { render() { return ( <> <li>1</li> <li>2</li> <li>3</li> </> ); } }
1)React.Fragment
<>和</>最終會被編譯成React.Fragment元件的開始和結束標籤,也就是說,前者是後者的語法糖。下面的程式碼和上一個Fragments的示例是等價的。
class Btns extends React.Component { render() { return ( <React.Fragment> <li>1</li> <li>2</li> <li>3</li> </React.Fragment> ); } }
如果要為Fragments新增Keys標識(即為其定義key屬性),那麼只能用React.Fragment元件包裹子元素。注意,key是React.Fragment元件目前唯一可用的屬性。
&n