詳解「react-dom」 API

語言: CN / TW / HK

highlight: agate theme: condensed-night-purple


「這是我參與11月更文挑戰的第3天,活動詳情檢視:2021最後一次更文挑戰」。

寫在前邊

Vue轉到React差不多快三個月,這兩種框架其實在設計哲學上完全是不一樣的道路但是同時又那麼相似。

最近在開發元件時遇到了一些需要關於Dom的操作,所以寫下這邊文章記錄下自己對於react-dom核心Api的理解,希望可以幫助到大家。

  • ReactDOM.render(element, container[, callback])

  • ReactDOM.unmountComponentAtNode(container)

  • ReactDOM.findDOMNode(component)

  • ReactDOM.createPortal(child, container)

文章會重點講述上述四個API,因為官網提供的描述是在是過於簡陋,所以這裡我會結合例子(通過人話)來重點講述他們的用法和適應場景,以及對比他們之間的差異性和相似性。

這篇文章的內容主要就是圍繞上邊四個API,比較基礎。如果你已經能夠完全熟練掌握他們的用法,那麼到這裡就可以啦!

Ok! Let's do it!

ReactDOM.render(element, container[, callback])

在提供的 container 裡渲染一個 React 元素,並返回對該元件的引用(或者針對無狀態元件返回 null)。

如果 React 元素之前已經在 container 裡渲染過,這將會對其執行更新操作,並僅會在必要時改變 DOM 以對映最新的 React 元素。

如果提供了可選的回撥函式,該回調將在元件被渲染或更新之後被執行。

單獨的ReactDom.render方法的確沒有什麼可講的,它的作用就是將我們傳入的JSX物件通過React.createElement(VDom)生成虛擬VDom,然後將生成的Vdom物件掛載真實Dom元素container元素上。

我們會在之後重點對比它和React.createPortal的區別。

同時,我們可以通過ReactDOM.unmountComponentAtNode(container)解除安裝對應的React.render(VNode,container)對應的節點和事件處理程式。

ReactDOM.unmountComponentAtNode(container)

從 DOM 中移除一個掛載的 React 元件並清理它的事件處理程式和狀態。如果容器中沒有安裝任何元件,則呼叫此函式什麼也不做。返回true是否已解除安裝元件以及false是否沒有要解除安裝的元件。

我們來看看他的型別定義,所謂的container就是Element | DocumentFragment:

code.png 針對unmountComponentAtNode,我們來用一個例子來稍微解釋一下它吧:

比如我們通過js建立一個div,它的內容是這樣的: const div = document.createElement('div'); HTML中的內容如下: ```html

接下來讓我們在`React`程式碼中執行下一句:js ReactDOM.render(, div); 此時我們通過`Components`元件中的元素建立了一個`VDom`元素,通過`render`方法將它渲染到上邊的`div`中去,我們得到這樣的`html`內容:html

(whatever HTML is created by Components)
``` 此時讓我們來執行最後一行程式碼: ```js ReactDOM.unmountComponentAtNode(div); ``` 刪除`Components`渲染到 div 中的元件,並清除與`Components`元件關聯的所有處理程式和 React 狀態(如果有的話)。 HTML 現在又是這個樣子: ```html
``` 通過這個簡單的例子我相信你已經能明白`unmountComponentAtNode`實現的作用了。 需要額外注意的是: + `unmountComponentAtNode`僅僅只能針對通過`ReactDom.render`**頂層掛載的元素進行解除安裝**。針對其他不相關Render方法元素是無效的(永遠返回false) + 之所以只能通過`unmountComponentAtNode`解除安裝頂層元件,這是`React`團隊刻意為之的。`React`希望子元件的解除安裝/渲染是通過父元件的狀態來控制,而不是直接通過操縱子元件。你可以[檢視這個回答來理解它。](https://stackoverflow.com/questions/36985738/how-to-unmount-unrender-or-remove-a-component-from-itself-in-a-react-redux-typ) # `ReactDOM.findDOMNode(component)` > 如果元件已經被掛載到 DOM 上,此方法會返回瀏覽器中相應的原生 DOM 元素。此方法對於從 DOM 中讀取值很有用,例如獲取表單欄位的值或者執行 DOM 檢測(performing DOM measurements)。**大多數情況下,你可以繫結一個 ref 到 DOM 節點上,可以完全避免使用 findDOMNode。** 簡單來說`findDOMNode`這個方法會返回傳入元件對應渲染真實的DOM節點,簡而言之也就是在`React`中獲取`Dom`的一種方式。 **大多數情況下,你可以繫結一個 ref 到 DOM 節點上,可以完全避免使用 findDOMNode。** > `findDOMNode`在嚴格模式下已經被`React`廢棄掉了。 # `ReactDOM.createPortal(child, container)` ## `createPortal`能為我們帶來什麼 關於`Portal`,我們先來考慮這樣一種業務場景。 假設我需要為我的專案定製化一款自己的`dialog`,你可能需要這麼一個元件去承載: ![code.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/aa81c6db72894aeba6040f8a8a7601e8~tplv-k3u1fbpfcp-watermark.image?) 當然這樣去開發元件的話會存在一些問題。 + 首先在元件結構層面,我們開發的Dialog元件和當前頁面上的結構是無關的,通常它是直接“蓋”在頁面之上的某個位置的。 比如,這樣: ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/661b4484eade4fff96c812cd21c7ec0a~tplv-k3u1fbpfcp-watermark.image?) **所以在結構上,我們希望它是可以獨立於頁面直接掛載在`body`元素上。但是目前我們這種寫法`Dialog`元件的結構會跟隨它的父元素巢狀在層級內。** 當然我們可以通過`position:fixed`達到我們想讓`dialog`在頁面中呈現的效果。但是這會引來另一個另一個致命的問題。 + 如果使用Fixed佈局讓`Dialog`定位後,會和業務強耦合。 Dialog設定為Fixed後,它的層級是基於**定位父元素**而決定的。**這也就導致瞭如果我們想要調整Dialog的層級的話它還依賴於巢狀父元素的定位**。這無疑是一種噩夢,作為一個通用元件來說它具有太多和業務的耦合,甚至還會被業務影響。 關於如何解決上述的問題就要引出來我們的主角了:`ReactDOM.createPortal(child, container)`。 > 這裡也許有的同學會疑惑,`ReactDom.render`和`ReactDom.createPortal`的區別。這裡我會在下邊給大家分析他們的區別。 ## `createPortal`簡介 Portal 提供了一種將子節點渲染到存在於父元件以外的 DOM 節點的優秀的方案。 我們可以通過`createPortal(vNode,dom)`在`React`中跳過層級關係將我們的`vNode`任何React元素渲染到指定的真實`Dom`元素上去。 熟悉`Vue3`的同學可能第一時間就想到`Teleport`,沒錯。你完全可以使用`Teleport`的思想來理解`createPortal`。 關於[createPortal](https://zh-hans.reactjs.org/docs/portals.html)你可以點選它來檢視更加詳盡的解釋。 這裡其實我想給大家重點講述的是 ### `ReactDOM.createPortal(child, container)`作用 簡單來說就一句話:**`createPortal`提供一種將`React`元素子節點渲染到真實`DOM`節點的方式。** **這個節點可以脫離於`React`中的`DOM`結構層次。** ### `ReactDOM.createPortal(child, container)`和`ReactDom.render(vNode,dom)`區別於聯絡 你可以在[codeopen中點選這個例子檢視程式碼對比](https://codesandbox.io/s/42x771ykwx?file=/index.js)。 同時也可以在[點選官網檢視](https://zh-hans.reactjs.org/docs/portals.html) 簡單來說**createPortal渲染的元素儘管可以出現在`DOM`結構中的任何地方,但是同時通過 Portal 仍然可以進行事件冒泡/context 傳遞之類的特性。** 你可以將它簡單的理解成為`Portal`元素僅僅是渲染時在脫離固定的結構而已,本質上它仍然是`React Tree`中固定位置的普通節點,所以它仍然可以進行`context`傳遞以及`React`事件冒泡等。 而針對於`ReactDom.render()`方法。這個方法根據傳入的`VDom`元素重新渲染了一個`React Tree`從而渲染掛載在對應的元素上。它已經脫離了原本的`React Tree`,自然而言就無法通過`React`事件冒泡機制觸發父元素的事件以及接受父元素的`Context`。(因為它本身就屬於原本的`React Tree`中,只是在你的程式碼結構中看起來他們一致而已)。 > 本質上造成這種原因的還是`React`中的`render`方法原理和合成事件,有興趣瞭解這部分原始碼的朋友可以移步[這裡](https://github.com/19Qingfeng/react-source-code)。(程式碼中有詳細的註釋,是我自己實現的一個簡單版本的`React`,目前已經完成合成事件章節)。 ### `ReactDOM.createPortal`拓展 關於`ReactDom.render`的使用方式這裡我不打算累贅,它已經出現很久了。眾所周知,它可以: + 在我們的業務頁面上直接通過`ReactDom.render(, document.getElementById('root'))`去控制頁面渲染。 + 在函式式`API`呼叫方式中大展身手,比如`antd`中的`message.success(config)`相關`APi`。 這裡,我想和大家重點聊聊`createPortal`。 我們先來看一看關於它的型別定義: ![code.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cad9d149d41c4266ac94b17ad5d1a0f0~tplv-k3u1fbpfcp-watermark.image?) 這裡我們可以看到,這個方法接受的引數型別和它返回的引數型別。 注意:它返回的是`ReactPortal`,繼承於`ReactElement`。 嗯,這裡我們瞭解了`Portal`的返回值本質上就可以當作`ReactElement`去使用,說白了它也就是`VDom`。 我們來列印它看看: ```js import React from 'react' import { createPortal } from 'react-dom' function Dialog() { const div = document.createElement('div') console.log(createPortal(

, div), '列印') return createPortal(

, div) } export default Dialog; ``` ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/418005ac56164b3e865e71008dac8560~tplv-k3u1fbpfcp-watermark.image?) 我們發現它比平常的`VDom`物件多出了一個`containerInfo`屬性,而這個屬性指向的節點正是我們上邊建立的`div`。 看到這裡我相信有部分同學已經明白了,沒錯`React`內部正是通過`containerInfo`來選擇當前`VNode`掛載的節點,當不存在`containerInfo`時他會遵循規則掛載,而當存在`containerInfo`時它會將傳入的`React`元素掛載在`containerInfo`對應的節點中去。 # 結尾 其實如果要深挖`ReactDom`的`API`還是能挖出不少知識點的,這裡我給大家帶來的僅僅是拋磚引玉,僅僅達到使用層面的講解。 感謝每一位看到結尾的同學,希望文中的知識可以帶給大家幫助。