摘要:引言組件中有很多彈出式組件,常見的如,以及等。這樣一種層次結(jié)構(gòu)在實(shí)踐中大大降低了各類彈層組件的實(shí)現(xiàn)和維護(hù)成本。但是的組件實(shí)現(xiàn)了一個(gè)大多數(shù)組件庫(kù)都沒有實(shí)現(xiàn)的功能彈層的嵌套處理。
引言
UI 組件中有很多彈出式組件,常見的如 Dialog,Tooltip 以及 Select 等。這些組件都有一個(gè)特點(diǎn),它們的彈出層通常不是渲染在當(dāng)前的 DOM 樹中,而是直接插入在 body (或者其它類似的地方)上的。這么做的主要目的是方便控制這些彈出層的 z-index ,確保它們能夠處于合適的層級(jí)上,不至于被遮擋。
我們都知道 React App 的頂層某個(gè)地方肯定有這么一行代碼:ReactDOM.render(
在 React 的這種管理模式下,會(huì)發(fā)現(xiàn)使用彈層似乎不太方便,因?yàn)榻M件樹是逐層往下生長(zhǎng)的,但React 的 API 中并沒有直接提供跳出這棵組件樹的方法[注1]。
所以,為了實(shí)現(xiàn)彈層組件,我們需要先實(shí)現(xiàn)一個(gè) Portal 組件(玩游戲的都知道,這是傳送門的意思),這個(gè)組件只做一件事:將組件樹中某些節(jié)點(diǎn)移出當(dāng)前的DOM 樹,并且渲染到指定的 DOM 節(jié)點(diǎn)中。
Portal 組件Portal 組件的要做的事情很簡(jiǎn)單,render 函數(shù)因?yàn)椴恍枰诋?dāng)前位置輸出任何東西,所以直接返回 null 就可以了,剩下的就是在組件的生命周期中去手動(dòng)管理要渲染到指定位置的那些組件。
// 簡(jiǎn)化的 Portal 實(shí)現(xiàn) class Portal extends Component { static propTypes = { children: PropTypes.node.isRequired, container: PropTypes.object.isRequired }; render() { return null; } componentDidMount() { const { children, container } = this.props; mountChildrenAtNode(children, container); } componentWillUnmount() { const { container } = this.props; unmountChildrenAtNode(container); } }
剩下唯一的問(wèn)題是 mountChildrenAtNode 這個(gè)函數(shù)怎么實(shí)現(xiàn)?仔細(xì)的同學(xué)應(yīng)該已經(jīng)發(fā)現(xiàn)了,這個(gè)函數(shù)和 ReactDOM.render 非常像,仔細(xì)一想,其實(shí)它們做的事情就是一樣的。所以我們直接用 ReactDOM.render 去替換 mountChildrenAtNode 就可以了。
那么真的這么簡(jiǎn)單嗎?
是,但也不是。
說(shuō)是,是因?yàn)檫壿嬌线@代碼并沒有什么問(wèn)題,而且大部分場(chǎng)景下是確實(shí)可以完美工作。
說(shuō)不是,是因?yàn)槭O碌男〔糠謭?chǎng)景下這段代碼確實(shí)存在很嚴(yán)重的問(wèn)題。
那么問(wèn)題是什么呢?別急,我們先聊點(diǎn)別的。
相信大部分 React 開發(fā)者都用過(guò) redux(至少聽過(guò)吧),react-redux 這個(gè) binding 庫(kù)提供了連接 React 和 redux 的一個(gè)橋梁。react-redux 的實(shí)現(xiàn)依賴 React 很有用的一個(gè)功能Context,簡(jiǎn)單來(lái)說(shuō) context 就是提供了一個(gè)方便的跨越層級(jí)往下傳遞數(shù)據(jù)的方式。
ReactDOM.render 的問(wèn)題正是在于這個(gè) context 的功能,它無(wú)法連接兩棵 React 組件樹的 context。
ReactDOM.render 的函數(shù)原型中并沒有當(dāng)前組件樹的信息,而 context 是跟組件樹有關(guān)的。
ReactDOM.render( element, container, [callback] )
解決這個(gè)問(wèn)題的方法也很簡(jiǎn)單,這里也不賣關(guān)子了,React 提供了另一個(gè)非公開 API:ReactDOM.unstable_renderSubtreeIntoContainer。這個(gè) API 多了一個(gè)參數(shù),這個(gè)參數(shù)就是用來(lái)指定新的 React 組件樹根節(jié)點(diǎn)的父組件的,有了這個(gè)參數(shù),兩棵本來(lái)互不相干的 React 組件樹就被聯(lián)系起來(lái)了,同時(shí)它們的 context 也連接了起來(lái)。
ReactDOM.unstable_renderSubtreeIntoContainer( parentComponent, element, container, [callback] )
想更好的了解 Context 的同學(xué)可以自己 Google,這不是本文重點(diǎn),這里不做展開了。
Portal 組件的可擴(kuò)展性不同的 UI 組件對(duì)彈層可能會(huì)有不同的功能需求,舉個(gè)例子, Dialog 組件需要在彈出的時(shí)候禁止頁(yè)面滾動(dòng),同時(shí)有些場(chǎng)景下需要支持點(diǎn)擊背景部分關(guān)閉,或者按 ESC 鍵關(guān)閉。
這些很細(xì)節(jié)的功能點(diǎn)往往會(huì)出現(xiàn)需要不同組合的使用場(chǎng)景,例如只需要禁止?jié)L動(dòng),或者同時(shí)需要禁止?jié)L動(dòng)和 ESC 鍵關(guān)閉。
一個(gè)很自然的想法是在 Portal 組件上加幾個(gè)可配置的 props 來(lái)控制這些功能。這么做有個(gè)問(wèn)題,不管用戶需不需要,代碼都在那里。
更好的方式是通過(guò)高階組件(HOC)的方式讓使用者自己去組合這些功能,這樣子沒有用到的功能并不會(huì)出現(xiàn)在最終的代碼中。
說(shuō)了這么多關(guān)于 Portal 組件的實(shí)現(xiàn)細(xì)節(jié),有興趣的同學(xué)可以去看看有贊的組件庫(kù) Zent 里面的 Portal 是如何實(shí)現(xiàn)的,大體上就是按上面說(shuō)的那些方案做的。
彈層組件有了 Portal 組件之后,基本上所有彈層組件都可以基于 Portal 去實(shí)現(xiàn)。例如 Dialog 無(wú)非就是在 Portal 組件的基礎(chǔ)上加了一些 CSS 樣式。復(fù)雜一點(diǎn)的組件例如 Select,需要實(shí)現(xiàn)一些觸發(fā)邏輯來(lái)控制彈層的打開和關(guān)閉,比如 click 打開或者 hover 打開。我們接下來(lái)要討論的彈層組件正是特指類似 Select 中的這些彈層。
在 Zent 里面有一個(gè)叫 Popover 的組件來(lái)處理這些復(fù)雜的彈層場(chǎng)景,Popover 封裝了常用的觸發(fā)邏輯,例如 click, hover, focus,同時(shí) Popover 的觸發(fā)機(jī)制是可擴(kuò)展的,使用者可以實(shí)現(xiàn)自己的觸發(fā)邏輯。
Popover 組件提供的另外一個(gè)重要功能是彈層的定位能力,也就是相對(duì)于 Trigger 的一個(gè)定位功能。除了內(nèi)置的十幾種定位算法,使用者可以實(shí)現(xiàn)自己的定位算法來(lái)實(shí)現(xiàn)特殊場(chǎng)景下的需求。
有了 Popover 組件提供的觸發(fā)邏輯以及彈層定位這兩個(gè)功能之后,類似 Tooltip , Select 這樣的組件在實(shí)現(xiàn)時(shí)就完全不需要關(guān)心彈層的事了,只需要實(shí)現(xiàn)彈層內(nèi)的組件邏輯就行了。
這里已經(jīng)能夠看出一個(gè)層次化的彈層組件設(shè)計(jì)了:Portal 負(fù)責(zé)脫離組件樹,Popover 在 Portal 的基礎(chǔ)上提供了更豐富的功能邏輯,其它組件又在 Popover 的基礎(chǔ)上去做封裝。這樣一種層次結(jié)構(gòu)在實(shí)踐中大大降低了各類彈層組件的實(shí)現(xiàn)和維護(hù)成本。
在組件庫(kù)的設(shè)計(jì)中,這種對(duì)能力的抽象封裝是很重要的,在提高開發(fā)效率的同時(shí)也保證了各個(gè)組件行為的一致性。
干貨:彈層組件的嵌套處理上面介紹的彈層組件實(shí)現(xiàn)細(xì)節(jié)上并沒有特別之處,成熟的組件庫(kù)基本都是用類似方式實(shí)現(xiàn)的。但是 Zent 的 Popover 組件實(shí)現(xiàn)了一個(gè)大多數(shù) React 組件庫(kù)都沒有實(shí)現(xiàn)的功能:彈層的嵌套處理。
如果你還沒有明白這里的彈層嵌套是什么意思,沒關(guān)系,給你舉個(gè)例子就明白了。
如下圖,點(diǎn)擊按鈕之后會(huì)彈出一個(gè)氣泡,這個(gè)氣泡中又有一個(gè)時(shí)間選擇器,所謂的彈層嵌套指的就是這種彈層之中又嵌了彈層的場(chǎng)景。正常的操作邏輯是鼠標(biāo)點(diǎn)擊位置1的時(shí)候氣泡和時(shí)間選擇器同時(shí)關(guān)閉,但是點(diǎn)擊位置2的時(shí)候應(yīng)該只有時(shí)間選擇器關(guān)閉。
上面提到的點(diǎn)擊兩個(gè)不同位置的不同行為其實(shí)就是彈層嵌套最主要的問(wèn)題:上級(jí)的彈層組件應(yīng)該知道哪個(gè)區(qū)域是屬于下級(jí)彈層組件的。
由于彈層組件的特殊性,它們?cè)?DOM 樹中的位置跟它們實(shí)際的層次以及包含關(guān)系是沒有必然聯(lián)系的,上圖中的兩個(gè)彈層是body 下面的兩個(gè)兄弟節(jié)點(diǎn),但從彈層的角度看它們是有層次關(guān)系的,并不是并列的。
通常來(lái)說(shuō),彈層的層次結(jié)構(gòu)也是一個(gè)樹狀結(jié)構(gòu),那么處理嵌套問(wèn)題最直接的想法就是每個(gè)彈層組件都各自維護(hù)一個(gè)子彈層的列表。當(dāng)需要判斷點(diǎn)擊是否在彈層外面時(shí),不光要考慮當(dāng)前彈層對(duì)應(yīng)的 DOM 節(jié)點(diǎn),還要考慮它的下級(jí)彈層對(duì)應(yīng)的 DOM 節(jié)點(diǎn)。
這種方式處理的話需要手動(dòng)維護(hù)這棵彈層的層級(jí)關(guān)系樹,包括樹中節(jié)點(diǎn)的插入/刪除,這些操作都不是很難。這個(gè)方法最大的問(wèn)題在于,在 React 的體系內(nèi)一個(gè)彈層組件很難跟不是它直接孩子(direct child)的子彈層交互。
Zent 的 Popover 組件并沒有直接去維護(hù)這棵層級(jí)關(guān)系樹,而是利用了 React 中 context 的層級(jí)關(guān)系來(lái)避免自己去維護(hù)這棵樹。使用 context 的另一個(gè)附帶好處是,和非直接孩子的交互也不再是問(wèn)題,因?yàn)?context 本身就是可以跨層級(jí)傳遞信息的。Popover 的層級(jí)管理結(jié)構(gòu)示意圖如下:
* context context * ------> ------> * Popover Root Popover child Popover grand-child ...... * <------ <------ * isOutsideQuery isOutsideQuery
就是這么一個(gè)很簡(jiǎn)單的設(shè)計(jì)解決了 Zent 中彈層組件的層級(jí)嵌套問(wèn)題,想了解實(shí)現(xiàn)細(xì)節(jié)的同學(xué)可以看 Popover 的源碼。
總結(jié)彈層組件是 UI 組件庫(kù)中很重要的部分,一個(gè)逐層抽象的結(jié)構(gòu)可以極大簡(jiǎn)化這些組件的開發(fā)和維護(hù)成本。
合理利用 React 的 context 功能可以很方便地解決一些像嵌套彈層一樣看似很麻煩的問(wèn)題。
如果覺得有所收獲,請(qǐng)給 Zent 點(diǎn)個(gè) star 吧。
*注1: React Fiber 中提供了一個(gè)新的 API:ReactDOM. unstable_createPortal ,這個(gè) API 可以將一個(gè)組件渲染到指定的 DOM 節(jié)點(diǎn)內(nèi)。
本文由 李晨 首發(fā)于 有贊技術(shù)博客。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/88434.html
摘要:前端日?qǐng)?bào)精選低成本將你的網(wǎng)站切換為漫談組件庫(kù)開發(fā)一多層嵌套彈層組件可作的備胎深入理解進(jìn)階系列如何設(shè)計(jì)中文刷題系列前端筆試面試題知乎專欄個(gè)拯救前端開發(fā)者的工具庫(kù)和資源眾成翻譯前端技術(shù)大會(huì)震撼登陸,明星團(tuán)隊(duì)講師傾城而出前端組件庫(kù)我們做 2017-09-08 前端日?qǐng)?bào) 精選 低成本將你的網(wǎng)站切換為 HTTPS漫談 React 組件庫(kù)開發(fā)(一):多層嵌套彈層組件Preact: 可作React的...
摘要:前端日?qǐng)?bào)精選譯中一些超級(jí)好用的內(nèi)置方法漫談組件庫(kù)開發(fā)一多層嵌套彈層組件高階組件淺析的工廠函數(shù)打包優(yōu)化之速度篇中文教程用純實(shí)現(xiàn)跳跳球動(dòng)畫眾成翻譯個(gè)幫助你學(xué)習(xí)的快速且久經(jīng)考驗(yàn)的技巧眾成翻譯自定義屬性使用進(jìn)行動(dòng)態(tài)更改眾成翻譯真假值知多 2017-08-26 前端日?qǐng)?bào) 精選 【譯】ES6中一些超!級(jí)!好!用!的內(nèi)置方法漫談 React 組件庫(kù)開發(fā)(一):多層嵌套彈層組件React 高階組件淺析...
摘要:又一篇來(lái)自日常開發(fā)的匯總各位客官請(qǐng)對(duì)號(hào)入席,店小二逐一上菜。解決方案有很多種,例如把字符串?dāng)?shù)組等重組對(duì)象數(shù)組,每個(gè)元素設(shè)置一個(gè)唯一等。另外有個(gè)方式推薦使用生成唯一的數(shù)組,和數(shù)據(jù)數(shù)組一起使用,省去提交數(shù)據(jù)時(shí)再重組數(shù)組。 又一篇來(lái)自日常開發(fā)的匯總:各位客官請(qǐng)對(duì)號(hào)入席,店小二逐一上菜。 第一道菜:回鍋肉 react數(shù)組循環(huán),基本都會(huì)設(shè)置一個(gè)唯一的key,表格的對(duì)象數(shù)組循環(huán)一般沒什么問(wèn)題,數(shù)據(jù)...
摘要:但是,最后一步,事件怎么綁定呢這塊沒有深入研究了,不過(guò)我想,應(yīng)該這樣去實(shí)現(xiàn)也是沒有問(wèn)題的。的具體做法是,把方法放到了一個(gè)叫做的組件上去實(shí)現(xiàn)這個(gè)功能,然后再把內(nèi)容放進(jìn)這個(gè)組件。其他的邏輯比如顯示隱藏之類,全部都放到組件自身上去實(shí)現(xiàn)。 1、Dialog組件提供什么功能,解決什么問(wèn)題? zent的Dialog組件,使用姿勢(shì)是這樣的(代碼摘自zent官方文檔:https://www.youza...
摘要:父組件向子組件之間非常常見,通過(guò)機(jī)制傳遞即可。我們應(yīng)該聽說(shuō)過(guò)高階函數(shù),這種函數(shù)接受函數(shù)作為輸入,或者是輸出一個(gè)函數(shù),比如以及等函數(shù)。在傳遞數(shù)據(jù)的時(shí)候,我們可以用進(jìn)一步提高性能。 本文主要談自己在react學(xué)習(xí)的過(guò)程中總結(jié)出來(lái)的一些經(jīng)驗(yàn)和資源,內(nèi)容邏輯參考了深入react技術(shù)棧一書以及網(wǎng)上的諸多資源,但也并非完全照抄,代碼基本都是自己實(shí)踐,主要為平時(shí)個(gè)人學(xué)習(xí)做一個(gè)總結(jié)和參考。 本文的關(guān)鍵...
閱讀 3056·2021-11-16 11:51
閱讀 2664·2021-09-22 15:02
閱讀 3823·2021-08-04 10:21
閱讀 3711·2019-08-30 15:43
閱讀 2015·2019-08-30 11:04
閱讀 3657·2019-08-29 17:14
閱讀 556·2019-08-29 12:16
閱讀 2989·2019-08-28 18:31