傳送門:React Portal
轉載自:https://zhuanlan.zhihu.com/p/29880992
React v16增加了對Portal的直接支援,今天我們就來聊一聊Portal。
似乎所有說React Portal都直接用Portal這個單詞,沒聽過這詞的朋友可能覺得不知所云,其實,Portal可以有一個很形象的翻譯——“傳送門”。
什麼是傳送門?
曾經有一款遊戲就叫做Portal,玩家手上一杆很厲害很科幻的槍,朝牆上開一槍,就可以開出兩個“傳送門”,人鑽進這個傳送門,可以從另一個傳送門裡走出來,也就是說,兩個不同位置的傳送門之間形成了對接。
如果還不明白Portal是啥,那就拿范冰冰在《X戰警:逆轉未來》所演角色的GIF動圖來看吧。
你看一個哨兵機器人撲過來攻擊一個X戰警,范冰冰從一個傳送門裡神速穿越而來,順手又甩出兩個傳送門,讓哨兵機器人撲進了一個傳送門,從另一個傳送門一個踉蹌掉了出來,從而救了那個X戰警。
現在明白Portal是怎麼回事了吧。
為什麼React需要傳送門?
React Portal之所以叫Portal,因為做的就是和“傳送門”一樣的事情:render到一個元件裡面去,實際改變的是網頁上另一處的DOM結構。
在React的世界中,一切都是元件(參見《幫助你深入理解React》),用元件可以表示一切介面中發生的邏輯,不過,有些特例處理起來還比較麻煩,比如,某個元件在渲染時,在某種條件下需要顯示一個對話方塊(Dialog),這該怎麼做呢?
最直觀的做法,就是直接在JSX中把Dialog畫出來,像下面程式碼的樣子。
<div class="foo">
<div> ... </div>
{ needDialog ? <Dialog /> : null }
</div>
問題是,我們寫一個Dialog元件,就這麼渲染的話,Dialog最終渲染產生的HTML就存在於上面JSX產生的HTML一起了,類似下面這樣。
<div class="foo">
<div> ... </div>
<div class="dialog">Dialog Content</div>
</div>
可是問題來了,對於對話方塊,從使用者感知角度,應該是一個獨立的元件,通常應該顯示在螢幕的最中間,現在Dialog被包在其他元件中,要用CSS的position屬性控制Dialog位置,就要求從Dialog往上一直到body沒有其他postion是relative的元素干擾,這……有點難為作為通用元件的Dialog,畢竟,誰管得住所有元件不用position呢。
還有一點,Dialog的樣式,因為包在其他元素中,各種樣式糾纏,CSS樣式太容易搞成一坨漿糊了。
看樣子這樣搞侷限很多啊,行不通,有沒有其他辦法?
有一個其他辦法,就是在React元件樹的最頂層留一個元素專屬於Dialog,然後通過Redux或者其他什麼通訊方式給這個Dialog傳送訊號,讓Dialog顯示或者不顯示。
這種方法看起來還湊合著,但是,就這點事還要動用Redux有點高射炮打蚊子,而且,要控制兩個不用位置的元件,好麻煩。
而且,如果我們把Dialog做成一個通用元件,希望裡面的內容完全定製,這招就更加麻煩了。
<div class="foo">
<div> ... </div>
{ needDialog ?
<Dialog>
<header>Any Header</header>
<section>Any content</section>
</Dialog>
: null }
</div>
像上面那樣,我們既希望在元件的JSX中選擇使用Dialog,把Dialog用得像一個普通元件一樣,但是又希望Dialog內容顯示在另一個地方,就需要Portal上場了。
Portal就是建立一個“傳送門”,讓Dialog這樣的元件在表示層和其他元件沒有任何差異,但是渲染的東西卻像經過傳送門一樣出現在另一個地方。
React在v16之前的傳送門實現方法
在v16之前,實現“傳送門”,要用到兩個祕而不宣的React API
- unstable_renderSubtreeIntoContainer
- unmountComponentAtNode
第一個unstable_renderSubtreeIntoContainer,都帶上字首unstable了,就知道並不鼓勵使用,但是沒辦法啊,不用也得用,還好React一直沒有deprecate這個API,一直挺到v16直接支援portal。這個API的作用就是建立“傳送門”,可以把JSX代表的元件結構塞到傳送門裡面去,讓他們在傳送門的另一端渲染出來。
第二個unmountComponentAtNode用來清理第一個API的副作用,通常在unmount的時候呼叫,不呼叫的話會造成資源洩露的。
一個通用的Dialog元件的實現差不多是這樣,注意看renderPortal中的註釋。
import React from 'react';
import {unstable_renderSubtreeIntoContainer, unmountComponentAtNode}
from 'react-dom';
class Dialog extends React.Component {
render() {
return null;
}
componentDidMount() {
const doc = window.document;
this.node = doc.createElement('div');
doc.body.appendChild(this.node);
this.renderPortal(this.props);
}
componentDidUpdate() {
this.renderPortal(this.props);
}
componentWillUnmount() {
unmountComponentAtNode(this.node);
window.document.body.removeChild(this.node);
}
renderPortal(props) {
unstable_renderSubtreeIntoContainer(
this, //代表當前元件
<div class="dialog">
{props.children}
</div>, // 塞進傳送門的JSX
this.node // 傳送門另一端的DOM node
);
}
}
首先,render函式不要返回有意義的JSX,也就說說這個元件通過正常生命週期什麼都不畫,要是畫了,那畫出來的HTML/DOM就直接出現在使用Dialog的位置了,這不是我們想要的。
在componentDidMount裡面,利用原生API來在body上建立一個div,這個div的樣式絕對不會被其他元素的樣式干擾。
然後,無論componentDidMount還是componentDidUpdate,都呼叫一個renderPortal來往“傳送門”裡塞東西。
總結,這個Dialog元件做得事情是這樣:
- 它什麼都不給自己畫,render返回一個null就夠了;
- 它做得事情是通過呼叫renderPortal把要畫的東西畫在DOM樹上另一個角落。
在renderPortal中,利用unstable_renderSubtreeIntoContainer函式往直前建立的div裡塞JSX,這裡我們用的JSX是這樣。
<div class="dialog">
{props.children}
</div>
因為是吧children畫出來,所以使用Dialog可以加上任意的子元件。
<Dialog>
What ever shit
<div>Hello</div>
<p>World</p>
</Dialog>
你看,所謂React Portal,就是能夠表面上渲染在一個地方,實際上渲染到了另一個地方。
是不是感覺好厲害,不光好厲害,而且像Dialog這樣的場景Portal簡直就是必不可少。
到了v16,React乾脆直接支援Portal,當然,v15還將被使用一段時間,所以大家看了上面的內容也不算浪費時間:-)
React v16的Portal支援
在v16中,使用Portal建立Dialog元件簡單多了,不需要牽扯到componentDidMount、componentDidUpdate,也不用呼叫API清理Portal,關鍵程式碼在render中,像下面這樣就行。
import React from 'react';
import {createPortal} from 'react-dom';
class Dialog extends React.Component {
constructor() {
super(...arguments);
const doc = window.document;
this.node = doc.createElement('div');
doc.body.appendChild(this.node);
}
render() {
return createPortal(
<div class="dialog">
{this.props.children}
</div>, //塞進傳送門的JSX
this.node //傳送門的另一端DOM node
);
}
componentWillUnmount() {
window.document.body.removeChild(this.node);
}
}
v16提供createPortal函式來建立“傳送門”,我個人覺得這個函式應該叫renderPortal好一些,因為元件的render函式除了mount時會被呼叫,update時也會被呼叫,update時還叫createPortal有點不大合適。
穿越Portal的事件冒泡
v16之前的React Portal實現方法,有一個小小的缺陷,就是Portal是單向的,內容通過Portal傳到另一個出口,在那個出口DOM上發生的事件是不會冒泡傳送回進入那一端的。
也就是說,這樣的程式碼。
<div onClick={onDialogClick}>
<Dialog>
What ever shit
</Dialog>
</div>
在Dialog畫出的內容上點選,onDialogClick是不會被觸發的。
當然,這只是一個小小的缺陷,大部分場景下事件不傳過來也沒什麼大問題。
在v16中,通過Portal渲染出去的DOM,事件是會冒泡從傳送門的入口端冒出來的,上面的onDialogClick也就會被呼叫到了。
相關文章
- ReactPortals傳送門React
- React Portal的前世今生React
- golang 學習傳送門Golang
- [傳智杯 #2 決賽] 傳送門
- lncRNA資料分析傳送門
- react-fetch資料傳送請求React
- Java Mail 郵件傳送(一):入門DemoJavaAI
- 通過Java傳送Email ,簡單入門。JavaAI
- flask入門4-檔案上傳與郵件傳送Flask
- 洛谷 P6464 [傳智杯 #2 決賽] 傳送門
- docker overlay儲存驅動介紹(傳送門)Docker
- 傳送陣
- 騰訊投資《傳送門騎士》開發商Keen GamesGAM
- Java入門:UDP協議傳送/接收資料實現JavaUDP協議
- 社群傳送門(其它社群的PHP相關好文章)PHP
- Java入門:TCP協議傳送/接收資料實現JavaTCP協議
- 郵件傳送
- 傳送郵件
- 收到263定時傳送郵件的傳送提醒
- C#原生郵件傳送+傳送日誌記錄C#
- 四種開源門戶portal軟體比較
- [iOS][OC] 開發利器:控制器傳送門VCPicker(附demo)iOS
- SpringBoot整合Mail傳送郵件&傳送模板郵件Spring BootAI
- 公眾號傳送模板資訊java實現(主動傳送)Java
- React傳-3React
- React傳-1React
- React傳-2React
- Laravel 傳送郵件Laravel
- PHP傳送郵件PHP
- Django——郵件傳送Django
- CURL 傳送檔案
- java郵件傳送Java
- Laravel傳送郵件Laravel
- DNS域傳送漏洞DNS
- gmail傳送郵件AI
- Oracle郵件傳送Oracle
- apache 傳送email demoApacheAI
- java傳送郵件Java