React Portal的前世今生

大隻魚00發表於2018-03-13

在設計UI元件的過程中不可避免的需要考慮模態窗的需求,比如dialog,tooltip這些,但是在React的框架下,我們似乎遇到了一些問題

React下的modal需求

通常在設計這些模態窗的時候,會把整個DOM結構儘量渲染在HTML位置比較頂層的地方,比如body。這樣相對來說樣式的自由度會比較高。 但是在React的整體框架下,它的資料流向是自上而下的,如果你的modal中的內容依賴父級的資料,那可能就要將對應的組建掛載在依賴組建裡面。當然可以在頂層元件管理modal資料或者直接上Redux,但是這對於一個定位成UI元件的設計上來說,顯示不夠合理。這便促成了portal的想法出現---希望modal元件 能跟正常的元件一樣不管哪裡需要就在哪裡掛載,但實際DOM的位置確是另外一個地方(比如React Bootstrap的Portal實現)

React16之前的實現思路

首先元件不能渲染在它掛載的地方

render() {
return null;
}
複製程式碼

DOM真正渲染的位置,通過renderLayer來實現

renderLayer() {
//這裡我們假定render的執行是輸出渲染內容
const { render } = this.props;
//構造DOM節點作為渲染內容的容器
if (!this.layer) {
this.layer = document.createElement('div');
document.body.appendChild(this.layer);
}
const layerElement = render();
this.layerElement = ReactDOM.unstable_renderSubtreeIntoContainer(this, layerElement, this.layer);
}

unrenderLayer() {
if (this.layer) {
React.unmountComponentAtNode(this.layer);
document.body.removeChild(this.layer);
this.layer = null;
}
}
複製程式碼

好了,我們在各個生命週期裡面呼叫它們就行了

ReactDOM中提供了一個unstable_renderSubtreeIntoContainer,從名字上就可以發現,它並不推薦被使用,實際上它也的確表現得令人費解。

class Test extends React.Component {
componentDidMount() {
console.log('test');
setTimeout(() => this.forceUpdate(),5000)
}
componentDidUpdate() {
console.log('did update test')
}
render() {
return <p>test<A/></p>;
}
}

class B extends React.Component {
componentDidMount() {
console.log('did mount B')
}
componentDidUpdate() {
console.log('did update B')
}
render() {
return <a>some thing</a>;
}
}

class A extends React.Component {
componentDidMount() {
this.renderLayer();
console.log('did mount A')
}
componentDidUpdate() {
this.renderLayer();
console.log('did update A')
}
renderLayer() {
if (!this.layer) {
this.layer = document.createElement('div');
document.body.appendChild(this.layer);
}
ReactDOM.unstable_renderSubtreeIntoContainer(this, <B/>, this.layer);
}
render() {
return null;
}
}

ReactDOM.render(<Test />, document.getElementById('app'));
複製程式碼

按我們對React父子元件間生命週期的執行情況上理解應當輸出 https://codepen.io/anon/pen/GQRaEo?editors=1112

"did mount B"
"did mount A"
"test"
"did update B"
"did update A"
"did update test"
複製程式碼

而實際的結果卻是

"did mount B"
"did mount A"
"test"
"did update A"
"did update test"
"did update B"
複製程式碼

顯然在初始化的時候事情還是符合我們預期的 可是在執行更新元件的時候,生命週期的執行便顯得很混亂,在React16的版本中這個問題得到了修復,但執行的結果顯然也不是我們最終想要的 https://codepen.io/anon/pen/MQWdPq?editors=1111

"did mount A"
"test"
"did mount B"
"did update A"
"did update test"
"did update B"
複製程式碼

React Portal的出現徹底解決了這方面的問題

React Portal

終於進入主題,先看看它是如何使用的

const node = document.createElement('div');
document.body.appendChild(this.node);
...

render() {
return createPortal(
<div class="dialog">
{this.props.children}
</div>, //需要渲染的內容
node //渲染內容的容器DOM
);
}
複製程式碼

除了node節點在一些場景下需要釋放之外,你已經不需要在其他生命週期裡面擦屁股了 讓我們在回到之前生命週期執行上的問題 https://codepen.io/anon/pen/Jpjqwg?editors=1111 結果的執行跟我們正常元件保持了一致,再也不用擔心一些依賴子元件完成更新後的監聽或操作會出現異常情況了。 除此之外React Portal還新增了一個事件冒泡的實現

<div onClick={handleClick}>
<Dialog/>
</div>
複製程式碼

如果在React16之前的實現方式,點選Dialog元件裡面的內容handleClick是不會被觸發,但通過React Portal實現的掛載方式將會發生冒泡。 這個特性見仁見智吧,一般情況下感覺也不會用到。

總結

總之React Portal的實現對於modal的實現是一個重大的更新,同時也避免了元件間生命週期的執行混亂。

相關文章