深度解析!Vue3 & React Hooks 新UI組件原理:Modal 彈窗

  • 2020 年 3 月 26 日
  • 筆記

前言

在某個月黑風高的晚上…沒劇刷的我無意想起以前處理的一些彈窗的坑。

然後又無意間刷到「Portal」,才知道Modal的實現還有如此妙的方式,順而想著乾脆把UI組件庫的實現原理看完。

本文將講述 Modal彈窗類的實現原理:

1. Modal彈窗的基本原理

我給彈窗類的定義是脫離固定的層級關係,不再受制於層疊上下文的組件。

常見的Modal模態框、Dialog對話框、Notification通知框等都是最最常用的交互方式。

在我們頁面有時需要一些特定的彈窗時,通過改UI組件過於麻煩。

這時切圖仔級別的會想:簡單啊,創建一個<div/> 給絕對定位不就得了。

倘若只是當前路由頁用,也還湊合。「可一旦涉及到了組件復用以及抽象為聲明式,就會有很大的隱患」

  1. 若無封裝,組件程式碼需要處處粘貼。
  2. 即使封裝了,都是在每個路由頁下創建<div/>,易造成樣式污染。
  3. 類購物車的彈窗,又該如何處理數據及渲染?
  4. 再進一步想,萬一組件庫會作為績效考核,拿到每個環境都長得不一樣,咋整?

1.1 Jquery時代的彈窗實現

初初入行時,去各種資源站,找JqueryUI組件,想必三四年經驗的前端們都曾樂此不疲。

這個時代(也就三四年前)的彈窗,因為沒有React/Vue根節點的概念,普遍都是:

  1. 「直接操作真實 dom,使用熟知的 dom 操作方法將指令所在的元素 append 到另外一個 dom 節點上去。」 如:document.body.appendChild
  2. 再通過overflow: hiddendisplay:none(或調整z-index)來隱藏。

這種操作真實dom的代價,在大型項目中不停觸發重繪/迴流,是很糟糕的,且內部數據/樣式不易更改。像以下這種情況就容易出現:

  1. 原本圖片固定在區域內。
  1. 小彈窗展示後,溢出了。

隨著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的實現都有類似的通病:

  1. 生命周期的執行會很混亂。
  2. 需要通過reduxprops管理數據,可這對於一個UI組件來說過於臃腫了。

React官方也意識到構建脫離於父組件的組件挺麻煩的,於是在v16版本推了一個叫「Portal 」的功能。而Vue3也是借鑒並吸納了優秀插件,將Portal作為內置組件了。

1.3 傳送門Portal方案

React / Vue的第二套方案都是基於操作虛擬dom

「定義一套組件,將組件內的 vnode/ReactDOM 轉移到另外一個組件中去,然後各自渲染。」

2. ReactPortal

React Portal之所以叫Portal,因為做的就是和「傳送門」一樣的事情:render到一個組件裡面去,實際改變的是網頁上另一處的DOM結構。

ReactDOM.createPortal(child, container)
  1. 第一個參數(child)是任何可渲染的 React 子元素,例如一個元素,字元串或碎片。
  2. 第二個參數(container)則是一個 DOM 元素。

v16中,使用Portal創建Dialog組件簡單多了,不需要牽扯到componentDidMountcomponentDidUpdate,也不用調用API清理Portal,關鍵程式碼在 render 中,像下面這樣就行:

‍當然,我們作為一個React Hooks選手,不騷一下咋行。

2.1 熱門組件庫Ant Design中的實現

原本是想從Ant Design庫中一窺究竟,卻發現事情並不簡單。。

前後定址了三個庫/地方,才發現實現的關鍵:

  1. import Dialog from 'rc-dialog';
  2. import Portal from 'rc-util/lib/PortalWrapper';
  3. import Portal from './Portal';

具體實現也算如我所料:

render里用了ReactDOM.createPortal`

**這也是為什麼多數Modal組件不會提供篡改整體樣式的API,只能通過全局重置樣式。`

2.1 React Hooks版彈窗:useModal

步驟一:創建一個Modal組件

步驟二:自定義useModal

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

步驟三:使用它

3. Vue3Portal

Vue雖說是借鑒,但使用方式可容易多了。

在上面的示例中,該<Modal />組件將在id=portal-target的容器中渲染,即使它位於OtherComponent組件內。

這,這…這也太香了吧。進一步的用法如下:

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

‍重要的解釋,都在上述注釋中了,臨時看的,說得不對的謝謝指正。

其中:createCommentVueDOM.createComment的進一步封裝。

結語&參考

這篇算是自己半夜無聊折騰出來的,原定計劃是一篇寫三種組件,但彈窗類的實現比較有意思。

這個系列我會看著寫,不出意外下一篇就是講Steps步驟條和Transfer穿梭框的實現(當然,太難了就忽悠一下,嘿嘿。)

「參考文章:」

  1. 《Building a simple and reusable modal with React hooks and portals》
  2. 《Vue 中的 Portal 技術 》
  3. 《Portal – a new feature in Vue 3》
  4. 《React Portal 的前世今生 》