深度解析!Vue3 & React Hooks 新UI組件原理:Modal 彈窗
- 2020 年 3 月 26 日
- 筆記
前言
在某個月黑風高的晚上…沒劇刷的我無意想起以前處理的一些彈窗的坑。

然後又無意間刷到「Portal
」,才知道Modal
的實現還有如此妙的方式,順而想著乾脆把UI
組件庫的實現原理看完。
本文將講述 Modal
彈窗類的實現原理:
1. Modal
彈窗的基本原理
我給彈窗類的定義是脫離固定的層級關係,不再受制於層疊上下文的組件。
常見的Modal
模態框、Dialog
對話框、Notification
通知框等都是最最常用的交互方式。

在我們頁面有時需要一些特定的彈窗時,通過改UI
組件過於麻煩。
這時切圖仔級別的會想:簡單啊,創建一個<div/>
給絕對定位不就得了。
倘若只是當前路由頁用,也還湊合。「可一旦涉及到了組件復用以及抽象為聲明式,就會有很大的隱患」:
- 若無封裝,組件程式碼需要處處粘貼。
- 即使封裝了,都是在每個路由頁下創建
<div/>
,易造成樣式污染。 - 類購物車的彈窗,又該如何處理數據及渲染?
- 再進一步想,萬一組件庫會作為績效考核,拿到每個環境都長得不一樣,咋整?

1.1 Jquery
時代的彈窗實現
初初入行時,去各種資源站,找Jquery
的UI
組件,想必三四年經驗的前端們都曾樂此不疲。

這個時代(也就三四年前)的彈窗,因為沒有React
/Vue
根節點的概念,普遍都是:
- 「直接操作真實 dom,使用熟知的 dom 操作方法將指令所在的元素 append 到另外一個 dom 節點上去。」 如:
document.body.appendChild
。 - 再通過
overflow: hidden
或display:none
(或調整z-index
)來隱藏。
這種操作真實dom
的代價,在大型項目中不停觸發重繪/迴流,是很糟糕的,且內部數據/樣式不易更改。像以下這種情況就容易出現:
- 原本圖片固定在區域內。

- 小彈窗展示後,溢出了。

隨著React / Vue
先進庫的發展,也陸續有了多種方案選擇。。。
1.2 React / Vue
早期實現。
其實React / Vue
早期的實現和Jquery
時代的並無二異:「依賴於父節點數據,在當前組件內掛載彈窗。」
Vue
的情況稍好,有自定義指令這條路走。
❝以下引自:《Vue 中的 Portal 技術》 ❞
以vue-dom-portal
為例,程式碼非常簡單無非就是將當前的 dom
移動到指定地方:

可以看到在 inserted
的時候就拿到實例的 el(真實 dom),然後進行替換操作,在 componentUpdated
的時候再次根據指令的值去操作 dom。
為了能夠在不同聲明周期函數中使用快取的一些數據,這裡在 inserted
的時候就把當前節點的父節點和替換成的 dom
節點(一個注釋節點),以及節點是否移出去的狀態都記錄在外部的一個 map
中,這樣可以在其他的聲明周期函數中使用,可以避免重複計算。
但是React / Vue
的實現都有類似的通病:
- 生命周期的執行會很混亂。
- 需要通過
redux
或props
管理數據,可這對於一個UI
組件來說過於臃腫了。
React
官方也意識到構建脫離於父組件的組件挺麻煩的,於是在v16
版本推了一個叫「Portal
」的功能。而Vue3
也是借鑒並吸納了優秀插件,將Portal
作為內置組件了。
1.3 傳送門Portal
方案

React / Vue
的第二套方案都是基於操作虛擬dom
:
「定義一套組件,將組件內的 vnode/ReactDOM
轉移到另外一個組件中去,然後各自渲染。」
2. React
的Portal
React Portal
之所以叫Portal
,因為做的就是和「傳送門」一樣的事情:render
到一個組件裡面去,實際改變的是網頁上另一處的DOM
結構。
ReactDOM.createPortal(child, container)
- 第一個參數(
child
)是任何可渲染的React
子元素,例如一個元素,字元串或碎片。 - 第二個參數(
container
)則是一個DOM
元素。
在v16
中,使用Portal
創建Dialog
組件簡單多了,不需要牽扯到componentDidMount
、componentDidUpdate
,也不用調用API
清理Portal
,關鍵程式碼在 render 中,像下面這樣就行:

當然,我們作為一個React Hooks
選手,不騷一下咋行。
2.1 熱門組件庫Ant Design
中的實現
原本是想從Ant Design
庫中一窺究竟,卻發現事情並不簡單。。

前後定址了三個庫/地方,才發現實現的關鍵:
import Dialog from 'rc-dialog';
import Portal from 'rc-util/lib/PortalWrapper';
import Portal from './Portal';
具體實現也算如我所料:


render里用了
ReactDOM.createPortal`
**這也是為什麼多數Modal
組件不會提供篡改整體樣式的API
,只能通過全局重置樣式。`
2.1 React Hooks
版彈窗:useModal

步驟一:創建一個Modal
組件

步驟二:自定義useModal

很好理解,不懂的建議轉行寫Vue
。

步驟三:使用它

3. Vue3
的Portal
Vue
雖說是借鑒,但使用方式可容易多了。

在上面的示例中,該<Modal />
組件將在id=portal-target
的容器中渲染,即使它位於OtherComponent
組件內。
這,這…這也太香了吧。進一步的用法如下:


然後我再去找了下Vue3
的源碼實現:在packages/runtime-core/src/components/Portal.ts
目錄中:

重要的解釋,都在上述注釋中了,臨時看的,說得不對的謝謝指正。
其中:createComment
是Vue
對DOM.createComment
的進一步封裝。
結語&參考
這篇算是自己半夜無聊折騰出來的,原定計劃是一篇寫三種組件,但彈窗類的實現比較有意思。

這個系列我會看著寫,不出意外下一篇就是講Steps
步驟條和Transfer
穿梭框的實現(當然,太難了就忽悠一下,嘿嘿。)
「參考文章:」
- 《Building a simple and reusable modal with React hooks and portals》
- 《Vue 中的 Portal 技術 》
- 《Portal – a new feature in Vue 3》
- 《React Portal 的前世今生 》