中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁 > news >正文

微信小程序開通要錢嗎seo超級外鏈發(fā)布

微信小程序開通要錢嗎,seo超級外鏈發(fā)布,??诰W(wǎng)站關(guān)鍵詞優(yōu)化,怎么找網(wǎng)站啊ReactPortals傳送門 React Portals提供了一種將子節(jié)點(diǎn)渲染到父組件以外的DOM節(jié)點(diǎn)的解決方案&#xff0c;即允許將JSX作為children渲染至DOM的不同部分&#xff0c;最常見用例是子組件需要從視覺上脫離父容器&#xff0c;例如對話框、浮動(dòng)工具欄、提示信息等。 描述 <div&…

ReactPortals傳送門

React Portals提供了一種將子節(jié)點(diǎn)渲染到父組件以外的DOM節(jié)點(diǎn)的解決方案,即允許將JSX作為children渲染至DOM的不同部分,最常見用例是子組件需要從視覺上脫離父容器,例如對話框、浮動(dòng)工具欄、提示信息等。

描述

<div><SomeComponent />{createPortal(children, domNode, key?)}
</div>

React Portals可以翻譯為傳送門,從字面意思上就可以理解為我們可以通過這個(gè)方法將我們的React組件傳送到任意指定的位置,可以將組件的輸出渲染到DOM樹中的任意位置,而不僅僅是組件所在的DOM層級內(nèi)。舉個(gè)簡單的例子,假設(shè)我們ReactDOM.render掛載組件的DOM結(jié)構(gòu)是<div id="root"></div>,那么對于同一個(gè)組件我們是否使用Portal在整個(gè)DOM節(jié)點(diǎn)上得到的效果是不同的:

export const App: FC = () => {return (<React.Fragment><div>123</div><div className="model"><div>456</div></div></React.Fragment>);
};// -> <body><div id="root"><div>123</div><div class="model"><div>456</div></div></div>
</body>
export const App: FC = () => {return (<React.Fragment><div>123</div>{ReactDOM.createPortal(<div className="model"><div>456</div></div>,document.body)}</React.Fragment>);
};// -> <body><div id="root"><div>123</div></div>{/* `DOM`結(jié)構(gòu)掛載到了`body`下 */}<div class="model"><div>456</div></div>
</body>

從上邊的例子中可以看到我們通過ReactDOM.createPortalReact組件掛載到了其他的DOM結(jié)構(gòu)下,在這里是掛載到了document.body下,當(dāng)然這這也是最常見的做法,這樣我們就可以通過Portal將組件傳送到目標(biāo)渲染的位置,由此來更靈活地控制渲染的行為,并解決一些復(fù)雜的UI交互場景,通常我們可以封裝Portal組件來更方便地調(diào)用。

export const Portal: React.FC = ({ children }) => {return typeof document === "object" ? ReactDOM.createPortal(children, document.body) : null;
};export const App: FC = () => (<Portal><SomeComponent /></Portal>
);

之前我們也聊到了,使用Portals最常見的場景就是對話框,或者可以認(rèn)為是浮動(dòng)在整個(gè)頁面頂部的組件,這樣的組件在DOM結(jié)構(gòu)上是脫離了父組件的,我們當(dāng)然可以自行實(shí)現(xiàn)相關(guān)的能力,例如主動(dòng)創(chuàng)建一個(gè)div結(jié)構(gòu)掛載到目標(biāo)DOM結(jié)構(gòu)下例如document.body下,然后利用ReactDOM.render將組建渲染到相關(guān)結(jié)構(gòu)中,在組件卸載時(shí)再將創(chuàng)建的div移除,這個(gè)方案當(dāng)然是可行的但是并沒有那么優(yōu)雅。當(dāng)然還有一個(gè)方法是使用狀態(tài)管理,在目標(biāo)組件中事先定義好相關(guān)的組件,通過狀態(tài)管理例如redux來控制顯隱,這種就是純粹的高射炮打蚊子,就沒有必要再展開了。

其實(shí)我們再想一想,既然我們是要脫離父組件結(jié)構(gòu)來實(shí)現(xiàn)這個(gè)能力,那么我們沒有必要非得使用PortalsCSSposition定位不是也可以幫助我們將當(dāng)前的DOM結(jié)構(gòu)脫離文檔流,也就是說我們沒必要將目標(biāo)組件的DOM結(jié)構(gòu)實(shí)際地分離出來,只需要借助position定位就可以實(shí)現(xiàn)效果。當(dāng)然想法是很美好的,真實(shí)場景就變得復(fù)雜的多了,那么脫離文檔流最常用的主要是絕對定位absolute與固定定位fixed。首先我們來看一下absolute,那么我們使用absolute其實(shí)很容易想到,我們需要從當(dāng)前組件一直到body都沒有其他positionrelative/absolute的元素,這個(gè)條件肯定是很難達(dá)到的,特別是如果我們寫的是一個(gè)組件庫的話,很難控制用戶究竟套了多少層以及究竟用什么CSS屬性。那么此時(shí)我們再將目光聚焦到fixed上,fixed是相對于視口來定位的,那么也就不需要像是absolute那么強(qiáng)的要求了,即使是父元素存在relative/absolute也沒有關(guān)系。當(dāng)然這件事沒有這么簡單,即使是fixed元素依舊可能會(huì)受到父元素樣式的影響,在這里舉兩個(gè)例子,分別是transformz-index

<!-- 不斷改變`transform: translateY(20px);`的值 `fixed`的元素也在不斷隨之變化 -->
<div style="transform: translateY(20px);"><div style="position: fixed; left: 10px; top: 10px;"><div style="background-color: blue; width: 10px; height: 10px;"></div></div>
</div><!-- 父級元素的`z-index`的層次比同級元素低 即使`fixed`元素`z-index`比父級高 也會(huì)被父級同級元素遮擋 -->
<divstyle="position: absolute; z-index: 100; width: 100px; height: 100px; background-color: #fff;"
></div>
<div style="position: absolute; z-index: 1"><div style="position: fixed; left: 10px; top: 10px; z-index: 1000"><div style="background-color: blue; width: 10px; height: 10px"></div></div>
</div>

從上邊的例子中我們可以看出,我們僅僅使用CSSposition定位是無法做到完全脫離父組件的,即使我們能夠達(dá)到脫離文檔流的效果,也會(huì)因?yàn)楦附M件的樣式而受到影響,特別是在組件庫中,我們作為第三方組件庫的話是完全沒有辦法控制用戶設(shè)計(jì)的DOM結(jié)構(gòu)的,如果僅僅采用脫離文檔流的方法而不實(shí)際將DOM結(jié)構(gòu)分離出來的話,那么我們的組件就會(huì)受到用戶樣式的影響,這是我們不希望看到的。此外,即使我們并不是設(shè)計(jì)組件庫,而僅僅是在我們的業(yè)務(wù)中實(shí)現(xiàn)相關(guān)需求,我們也不希望我們的組件受到父組件的影響,因?yàn)榧词棺铋_始我們的結(jié)構(gòu)和樣式?jīng)]出現(xiàn)問題,隨著業(yè)務(wù)越來越復(fù)雜,特別是多人協(xié)作開發(fā)項(xiàng)目,就很容易留下隱患,造成一些不必要的問題,當(dāng)然我們可以引入E2E來避免相關(guān)問題,這就是另一方面的解決方案了。

綜上,React Portals提供了一種更靈活地控制渲染的行為,可以用于解決一些復(fù)雜的UI交互場景,下面是一些常見的應(yīng)用場景:

  • 模態(tài)框和對話框: 使用Portals可以將模態(tài)框或?qū)υ捒蚪M件渲染到DOM樹的頂層,確保其可以覆蓋其他組件,并且在層級上獨(dú)立于其他組件,這樣可以避免CSSz-index屬性的復(fù)雜性,并且在組件層級之外創(chuàng)建一個(gè)干凈的容器。
  • 與第三方庫的集成: 有時(shí)候,我們可能需要將React組件與第三方庫(例如地圖庫或視頻播放器)集成,使用Portals可以將組件渲染到第三方庫所需的DOM元素中,即將業(yè)務(wù)需要的額外組件渲染到原組件封裝好的DOM結(jié)構(gòu)中,以確保組件在正確的位置和上下文中運(yùn)行。
  • 邏輯分離和組件復(fù)用: Portals允許我們將組件的渲染輸出與組件的邏輯分離,我們可以將組件的渲染輸出定義在一個(gè)單獨(dú)的Portal組件中,并在需要的地方使用該Portal,這樣可以實(shí)現(xiàn)組件的復(fù)用,并且可以更好地組織和管理代碼。
  • 處理層疊上下文: 在某些情況下,使用Portals可以幫助我們解決層疊上下文stacking context的問題,由于Portals可以創(chuàng)建獨(dú)立的DOM渲染容器,因此可以避免由于層疊上下文導(dǎo)致的樣式和布局問題。

MouseEnter事件

即使React Portals可以將組件傳送到任意的DOM節(jié)點(diǎn)中,但是其行為和普通的React組件一樣,其并不會(huì)脫離原本的React組件樹,這其實(shí)是一件非常有意思的事情,因?yàn)檫@樣會(huì)看起來,我們可以利用這個(gè)特性來實(shí)現(xiàn)比較復(fù)雜的交互。但是在這之前,我們來重新看一下MouseEnterMouseLeave以及對應(yīng)的MouseOverMouseOut的原生DOM事件。

  • MouseEnter: 當(dāng)鼠標(biāo)光標(biāo)進(jìn)入一個(gè)元素時(shí)觸發(fā),該事件僅在鼠標(biāo)從元素的外部進(jìn)入時(shí)觸發(fā),不會(huì)對元素內(nèi)部的子元素產(chǎn)生影響。例如,如果有一個(gè)嵌套的DOM結(jié)構(gòu)<div id="a"><div id="b"></div></div>,此時(shí)我們在元素a上綁定了MouseEnter事件,當(dāng)鼠標(biāo)從該元素外部移動(dòng)到內(nèi)部時(shí),MouseEnter事件將被觸發(fā),而當(dāng)我們再將鼠標(biāo)移動(dòng)到b元素時(shí),不會(huì)再次觸發(fā)MouseEnter事件。
  • MouseLeave:當(dāng)鼠標(biāo)光標(biāo)離開一個(gè)元素時(shí)觸發(fā),該事件僅在鼠標(biāo)從元素內(nèi)部離開時(shí)觸發(fā),不會(huì)對元素外部的父元素產(chǎn)生影響。例如,如果有一個(gè)嵌套的DOM結(jié)構(gòu)<div id="a"><div id="b"></div></div>,此時(shí)我們在元素a上綁定了MouseEnter事件,當(dāng)鼠標(biāo)從該元素內(nèi)部移動(dòng)到外部時(shí),MouseLeave事件將被觸發(fā),而如果此時(shí)我們的鼠標(biāo)是從b元素移出到a元素內(nèi),不會(huì)觸發(fā)MouseEnter事件。
  • MouseOver: 當(dāng)鼠標(biāo)光標(biāo)進(jìn)入一個(gè)元素時(shí)觸發(fā),該事件在鼠標(biāo)從元素的外部進(jìn)入時(shí)觸發(fā),并且會(huì)冒泡到父元素。例如,如果有一個(gè)嵌套的DOM結(jié)構(gòu)<div id="a"><div id="b"></div></div>,此時(shí)我們在元素a上綁定了MouseOver事件,當(dāng)鼠標(biāo)從該元素外部移動(dòng)到內(nèi)部時(shí),MouseOver事件將被觸發(fā),而當(dāng)我們再將鼠標(biāo)移動(dòng)到b元素時(shí),由于冒泡會(huì)再次觸發(fā)綁定在a元素上的MouseOver事件,再從b元素移出到a元素時(shí)會(huì)再次觸發(fā)MouseOver事件。
  • MouseOut: 當(dāng)鼠標(biāo)光標(biāo)離開一個(gè)元素時(shí)觸發(fā),該事件在鼠標(biāo)從元素內(nèi)部離開時(shí)觸發(fā),并且會(huì)冒泡到父元素。例如,如果有一個(gè)嵌套的DOM結(jié)構(gòu)<div id="a"><div id="b"></div></div>,此時(shí)我們在元素a上綁定了MouseOut事件,當(dāng)鼠標(biāo)從該元素內(nèi)部移動(dòng)到外部時(shí),MouseOut事件將被觸發(fā),而如果此時(shí)我們的鼠標(biāo)是從b元素移出到a元素內(nèi),由于冒泡會(huì)同樣觸發(fā)綁定在MouseOut事件,再從a元素移出到外部時(shí),同樣會(huì)再次觸發(fā)MouseOut事件。

需要注意的是MouseEnter/MouseLeave是在捕獲階段執(zhí)行事件處理函數(shù)的,而不能在冒泡階段過程中進(jìn)行,而MouseOver/MouseOut是可以在捕獲階段和冒泡階段選擇一個(gè)階段來執(zhí)行事件處理函數(shù)的,這個(gè)就看在addEventListener如何處理了。實(shí)際上兩種事件流都是可以阻斷的,只不過MouseEnter/MouseLeave需要在捕獲階段來stopPropagation,一般情況下是不需要這么做的。我個(gè)人還是比較推薦使用MouseEnter/MouseLeave,主要有這么幾點(diǎn)理由:

  • 避免冒泡問題: MouseEnterMouseLeave事件不會(huì)冒泡到父元素或其他元素,只在鼠標(biāo)進(jìn)入或離開元素本身時(shí)觸發(fā),這意味著我們可以更精確地控制事件的觸發(fā)范圍,更準(zhǔn)確地處理鼠標(biāo)交互,而不會(huì)受到其他元素的干擾,提供更好的用戶體驗(yàn)。
  • 避免重復(fù)觸發(fā): MouseOverMouseOut事件在鼠標(biāo)懸停在元素內(nèi)部時(shí)會(huì)重復(fù)觸發(fā),當(dāng)鼠標(biāo)從一個(gè)元素移動(dòng)到其子元素時(shí),MouseOut事件會(huì)在父元素觸發(fā)一次,然后在子元素觸發(fā)一次,MouseOut事件也是同樣會(huì)多次觸發(fā),可以將父元素與所有子元素都看作獨(dú)立區(qū)域,而事件會(huì)冒泡到父元素來執(zhí)行事件綁定函數(shù),這可能導(dǎo)致重復(fù)的事件處理和不必要的邏輯觸發(fā),而MouseEnterMouseLeave事件不會(huì)重復(fù)觸發(fā),只在鼠標(biāo)進(jìn)入或離開元素時(shí)觸發(fā)一次。
  • 簡化交互邏輯: MouseEnterMouseLeave事件的特性使得處理鼠標(biāo)移入和移出的交互邏輯變得更直觀和簡化,我們可以僅關(guān)注元素本身的進(jìn)入和離開,而不需要處理父元素或子元素的事件,這種簡化有助于提高代碼的可讀性和可維護(hù)性。

當(dāng)然究竟使用MouseEnter/MouseLeave還是MouseEnter/MouseLeave事件還是要看具體的業(yè)務(wù)場景,如果需要處理鼠標(biāo)移入和移出元素的子元素時(shí)或者需要利用冒泡機(jī)制來實(shí)現(xiàn)功能,那么MouseOverMouseOut事件就是更好的選擇,MouseEnter/MouseLeave能提供更大的靈活性和控制力,讓我們能夠創(chuàng)建復(fù)雜的交互效果,并更好地處理用戶與元素的交互,當(dāng)然應(yīng)用的復(fù)雜性也會(huì)相應(yīng)提高。

讓我們回到MouseEnter/MouseLeave事件本身上,在這里https://codesandbox.io/p/sandbox/trigger-component-1hv99o?file=/src/components/mouse-enter-test.tsx:1,1提供了一個(gè)事件的DEMO可以用來測試事件效果。需要注意的是,在這里我們是借助于React的合成事件來測試的,而在測試的時(shí)候也可以比較明顯地發(fā)現(xiàn)MouseEnter/MouseLeaveTS提示是沒有Capture這個(gè)選項(xiàng)的,例如Click事件是有onClickonClickCapture來表示冒泡和捕獲階段事件綁定的,而即使是在React合成事件中MouseEnter/MouseLeave也只會(huì)在捕獲階段執(zhí)行,所以沒有Capture事件綁定屬性。

--------------------------
|    c |      b |      a |
|      |        |        |
|-------        |        |
|               |        |
|----------------        |
|                        |
--------------------------

我們分別在三個(gè)DOM上都綁定了MouseEnter事件,當(dāng)我們鼠標(biāo)移動(dòng)到a上時(shí),會(huì)執(zhí)行a元素綁定的事件,當(dāng)依次將鼠標(biāo)移動(dòng)到a、bc的時(shí)候,同樣會(huì)以此執(zhí)行a、bc的事件綁定函數(shù),并且不會(huì)因?yàn)槊芭菔录?dǎo)致父元素事件的觸發(fā),當(dāng)我們鼠標(biāo)直接移動(dòng)到c的時(shí)候,可以看到依舊是按照a、b、c的順序執(zhí)行,也可以看出來MouseEnter事件是依賴于捕獲階段執(zhí)行的。

Portal事件

在前邊也提到了,盡管React Portals可以被放置在DOM樹中的任何地方,但在任何其他方面,其行為和普通的React子節(jié)點(diǎn)行為一致。我們都知道React自行維護(hù)了一套基于事件代理的合成事件,那么由于Portal仍存在于原本的React組件樹中,這樣就意味著我們的React事件實(shí)際上還是遵循原本的合成事件規(guī)則而與DOM樹中的位置無關(guān),那么我們就可以認(rèn)為其無論其子節(jié)點(diǎn)是否是Portal,像合成事件、Context這樣的功能特性都是不變的,下面是一些使用React Portals需要關(guān)注的點(diǎn):

  • 事件冒泡會(huì)正常工作: 合成事件將通過冒泡傳播到React樹的祖先,事件冒泡將按預(yù)期工作,而與DOM中的Portal節(jié)點(diǎn)位置無關(guān)。
  • React以控制Portal節(jié)點(diǎn)及其生命周期: Portal未脫離React組件樹,當(dāng)通過Portal渲染子組件時(shí),React仍然可以控制組件的生命周期。
  • Portal只影響DOM結(jié)構(gòu): 對于React來說Portal僅僅是視覺上渲染的位置變了,只會(huì)影響HTMLDOM結(jié)構(gòu),而不會(huì)影響React組件樹。
  • 預(yù)定義的HTML掛載點(diǎn): 使用React Portal時(shí),我們需要提前定義一個(gè)HTML DOM元素作為Portal組件的掛載。

在這里https://codesandbox.io/p/sandbox/trigger-component-1hv99o?file=/src/components/portal-test.tsx:1,1提供了一個(gè)PortalsMouseEnter事件的DEMO可以用來測試效果。那么在代碼中實(shí)現(xiàn)的嵌套精簡如下:

-------------------
|               a |
|           ------|------  --------
|           |     |   b |  |    c | 
|           |     |     |  |      |
|           |     |     |  --------
|           ------|------
-------------------
const C = ReactDOM.createPortal(<div onMouseEnter={e => console.log("c", e)}></div>, document.body);
const B = ReactDOM.createPortal(<React.Fragment><div onMouseEnter={e => console.log("b", e)}>{C}</div></React.Fragment>,document.body
);
const App = (<React.Fragment><div onMouseEnter={e => console.log("a", e)}></div>{B}</React.Fragment>
);// ==>const App = (<React.Fragment><div onMouseEnter={e => console.log("a", e)}></div>{ReactDOM.createPortal(<React.Fragment><div onMouseEnter={e => console.log("b", e)}>{ReactDOM.createPortal(<div onMouseEnter={e => console.log("c", e)}></div>,document.body)}</div></React.Fragment>,document.body)}</React.Fragment>
);

單純從代碼上來看,這就是一個(gè)很簡單的嵌套結(jié)構(gòu),而因?yàn)閭魉烷TPortals的存在,在真實(shí)的DOM結(jié)構(gòu)上,這段代碼結(jié)構(gòu)表現(xiàn)的效果是這樣的,其中id只是用來標(biāo)識ReactDOM結(jié)構(gòu),實(shí)際并不存在:

<body><div id="root"><div id="a"></div></div><div id="b"></div><div id="c"></div><div>
</body>

接下來我們依次來試試定義的MouseEnter事件觸發(fā)情況,首先鼠標(biāo)移動(dòng)到a元素上,控制臺(tái)打印a,符合預(yù)期,接下來鼠標(biāo)移動(dòng)到b元素上,控制臺(tái)打印b,同樣符合預(yù)期,那么接下來將鼠標(biāo)移動(dòng)到c,神奇的事情來了,我們會(huì)發(fā)現(xiàn)會(huì)先打印b再打印c,而不是僅僅打印了c,由此我們可以得到雖然看起來DOM結(jié)構(gòu)不一樣了,但是在React樹中合成事件依然保持著嵌套結(jié)構(gòu),C組件作為B組件的子元素,在事件捕獲時(shí)依然會(huì)從B -> C觸發(fā)MouseEnter事件,基于此我們可以實(shí)現(xiàn)非常有意思的一件事情,多級嵌套的彈出層。

Trigger彈出層

實(shí)際上上邊聊的內(nèi)容都是都是為這部分內(nèi)容做鋪墊的,因?yàn)楣ぷ鞯年P(guān)系我使用ArcoDesign是非常多的,又由于我實(shí)際是做富文本文檔的,需要彈出層來做交互的地方就非常多,所以在平時(shí)的工作中會(huì)大量使用ArcoDesignTrigger組件https://arco.design/react/components/trigger,之前我一直非常好奇這個(gè)組件的實(shí)現(xiàn),這個(gè)組件可以無限層級地嵌套,而且當(dāng)多級彈出層組件的最后一級鼠標(biāo)移出之后,所有的彈出層都會(huì)被關(guān)閉,最主要的是我們只是將其嵌套做了一層業(yè)務(wù)實(shí)現(xiàn),并沒有做任何的通信傳遞,所以我也一直好奇這部分的實(shí)現(xiàn),直到前一段時(shí)間我為了解決BUG深入研究了一下相關(guān)實(shí)現(xiàn),發(fā)現(xiàn)其本質(zhì)還是利用React Portals以及React樹的合成事件來完成的,這其中還是有很多交互實(shí)現(xiàn)可以好好學(xué)習(xí)下的。

同樣的,在這里也完成了一個(gè)DEMO實(shí)現(xiàn)https://codesandbox.io/p/sandbox/trigger-component-1hv99o?file=/src/components/trigger-simple.tsx:1,1,而在調(diào)用時(shí),則直接嵌套即可實(shí)現(xiàn)兩層彈出層,當(dāng)我們鼠標(biāo)移動(dòng)到a元素時(shí),b元素與c元素會(huì)展示出來,當(dāng)我們將鼠標(biāo)移動(dòng)到c元素時(shí),d元素會(huì)被展示出來,當(dāng)我們繼續(xù)將鼠標(biāo)快速移動(dòng)到d元素時(shí),所有的彈出層都不會(huì)消失,當(dāng)我們直接將鼠標(biāo)從d元素移動(dòng)到空白區(qū)域時(shí),所有的彈出層都會(huì)消失,如果我們將其移動(dòng)到b元素,那么只有d元素會(huì)消失。

-------------------  -------------  --------
|               a |  |         b |  |    d | 
|                 |  |--------   |  |      |
|                 |  |      c |  |  --------    
|                 |  |--------   |      
|                 |  -------------   
|                 | 
-------------------
<TriggerSimpleduration={200}popup={() => (<div id="b" style={{ height: 100, width: 100, backgroundColor: "green" }}><TriggerSimplepopup={() => <div id="d" style={{ height: 50, width: 50, backgroundColor: "blue" }}></div>}duration={200}><div id="c" style={{ paddingTop: 20 }}>Hover</div></TriggerSimple></div>)}
><div id="a" style={{ height: 150, width: 150, backgroundColor: "red" }}></div>
</TriggerSimple>

讓我們來拆解一下代碼實(shí)現(xiàn),首先是Portal組件的封裝,在這里我們就認(rèn)為我們將要掛載的組件是在document.body上的就可以了,因?yàn)槲覀円龅氖菑棾鰧?#xff0c;在最開始的時(shí)候也闡明了我們的彈出層DOM結(jié)構(gòu)需要掛在最外層而不能直接嵌套地放在DOM結(jié)構(gòu)中,當(dāng)然如果能夠保證不會(huì)出現(xiàn)相關(guān)問題,滾動(dòng)容器不是body的情況且需要position absolute的情況下,可以通過getContainer傳入DOM節(jié)點(diǎn)來制定傳送的位置,當(dāng)然在這里我們認(rèn)為是body就可以了。在下面這段實(shí)現(xiàn)中我們就通過封裝Portal組件來調(diào)度DOM節(jié)點(diǎn)的掛載和卸載,并且實(shí)際的組件也會(huì)被掛載到我們剛創(chuàng)建的節(jié)點(diǎn)上。

// trigger-simple.tsx
getContainer = () => {const popupContainer = document.createElement("div");popupContainer.style.width = "100%";popupContainer.style.position = "absolute";popupContainer.style.top = "0";popupContainer.style.left = "0";this.popupContainer = popupContainer;this.appendToContainer(popupContainer);return popupContainer;
};// portal.tsx
const Portal = (props: PortalProps) => {const { getContainer, children } = props;const containerRef = useRef<HTMLElement | null>(null);const isFirstRender = useIsFirstRender();if (isFirstRender || containerRef.current === null) {containerRef.current = getContainer();}useEffect(() => {return () => {const container = containerRef.current;if (container && container.parentNode) {container.parentNode.removeChild(container);containerRef.current = null;}};}, []);return containerRef.current? ReactDOM.createPortal(children, containerRef.current): null;
};

接下來我們來看構(gòu)造在React樹中的DOM結(jié)構(gòu),這塊可以說是整個(gè)實(shí)現(xiàn)的精髓,可能會(huì)比較繞,可以認(rèn)為實(shí)際上每個(gè)彈出層都分為了兩塊,一個(gè)是原本的child,另一個(gè)是彈出的portal,這兩個(gè)結(jié)構(gòu)是平行的放在React DOM樹中的,那么在多級彈出層之后,實(shí)際上每個(gè)子trigger(portal + child)都是上層portalchildren,這個(gè)結(jié)構(gòu)可以用一個(gè)樹形結(jié)構(gòu)來表示。

<React.Fragment>{childrenComponent}{portal}
</React.Fragment>
                         ROOT/    \A(portal)      A(child)/     \B(portal)      B(child)/     \C(portal)     C(child)/     \
.....   ..... 
<body><div id="root"><!-- ... --><div id="A-child"></div><!-- ... --></div><div id="A-portal"><div id="B-child"></div></div><div id="B-portal"><div id="C-child"></div></div><div id="C-portal"><!-- ... --></div>
</body>

從樹形結(jié)構(gòu)中我們可以看出來,雖然在DOM結(jié)構(gòu)中我們現(xiàn)實(shí)出來是平鋪的結(jié)構(gòu),但是在React的事件樹中卻依舊保持著嵌套結(jié)構(gòu),那么我們就很容易解答最開始的一個(gè)問題,為什么我們可以無限層級地嵌套,而且當(dāng)多級彈出層組件的最后一級鼠標(biāo)移出之后,所有的彈出層都會(huì)被關(guān)閉,就是因?yàn)閷?shí)際上即使我們的鼠標(biāo)在最后一級,但是在React樹結(jié)構(gòu)中其依舊是屬于所有portal的子元素,既然其是child那么實(shí)際上我們可以認(rèn)為其并沒有移出各級trigger的元素,自然不會(huì)觸發(fā)MouseLeave事件來關(guān)閉彈出層,如果我們移出了最后一級彈出層到空白區(qū)域,那么相當(dāng)于我們移出了所有trigger實(shí)例的portal元素區(qū)域,自然會(huì)觸發(fā)所有綁定的MouseLeave事件來關(guān)閉彈出層。

那么雖然上邊我們雖然解釋了Trigger組件為什么能夠維持無限嵌套層級結(jié)構(gòu)下能夠維持彈出層的顯示,并且在最后一級鼠標(biāo)移出之后能夠關(guān)閉所有彈出層,或者從最后一級返回到上一級只關(guān)閉最后一級彈出層,但是我們還有一個(gè)問題沒有想明白,上邊的問題是因?yàn)樗械?code>trigger彈出層實(shí)例都是上一級trigger彈出層實(shí)例的子元素,那么我們還有一個(gè)平級的portalchild元素呢,當(dāng)我們鼠標(biāo)移動(dòng)到child時(shí),portal元素會(huì)展示出來,而此時(shí)我們將鼠標(biāo)移動(dòng)到portal元素時(shí),這個(gè)portal元素并不會(huì)消失,而是會(huì)一直保持顯示,在這里的React樹是不存在嵌套結(jié)構(gòu)的,所以這里需要對事件進(jìn)行特殊處理。

onMouseEnter = (e: React.SyntheticEvent<HTMLDivElement, MouseEvent>) => {console.log("onMouseEnter", this.childrenDom);const mouseEnterDelay = this.props.duration;this.clearDelayTimer();his.setPopupVisible(true, mouseEnterDelay || 0);
};onMouseLeave = (e: React.SyntheticEvent<HTMLDivElement, MouseEvent>) => {console.log("onMouseLeave", this.childrenDom);const mouseLeaveDelay = this.props.duration;this.clearDelayTimer();if (this.state.popupVisible) {this.setPopupVisible(false, mouseLeaveDelay || 0);}
};onPopupMouseEnter = () => {console.log("onPopupMouseEnter", this.childrenDom);this.clearDelayTimer();
};onPopupMouseLeave = (e: React.SyntheticEvent<HTMLDivElement, MouseEvent>) => {console.log("onPopupMouseLeave", this.childrenDom);const mouseLeaveDelay = this.props.duration;this.clearDelayTimer();if (this.state.popupVisible) {this.setPopupVisible(false, mouseLeaveDelay || 0);}
};setPopupVisible = (visible: boolean, delay = 0, callback?: () => void) => {onst currentVisible = this.state.popupVisible;if (visible !== currentVisible) {this.delayToDo(delay, () => {if (visible) {this.setState({ popupVisible: true }, () => {this.showPopup(callback);});} else {this.setState({ popupVisible: false }, () => {callback && callback();});}});} else {callback && callback();}
};delayToDo = (delay: number, callback: () => void) => {if (delay) {this.clearDelayTimer();this.delayTimer = setTimeout(() => {callback();this.clearDelayTimer();}, delay);} else {callback();}
};

實(shí)際上在這里的通信會(huì)比較簡單,之前我們也提到portalchild元素是平級的,那么我們可以明顯地看出來實(shí)際上這是在一個(gè)組件內(nèi)的,那么整體的實(shí)現(xiàn)就會(huì)簡單很多,我們可以設(shè)計(jì)一個(gè)延時(shí),并且可以為portalchild分別綁定MouseEnterMouseLeave事件,在這里我們?yōu)?code>child綁定的是onMouseEnteronMouseLeave兩個(gè)事件處理函數(shù),為portal綁定了onPopupMouseEnteronPopupMouseLeave兩個(gè)事件處理函數(shù)。那么此時(shí)我們模擬一下上邊的情況,當(dāng)我們鼠標(biāo)移入child元素時(shí),會(huì)觸發(fā)onMouseEnter事件處理函數(shù),此時(shí)我們會(huì)清除掉delayTimer,然后會(huì)調(diào)用setPopupVisible方法,此時(shí)會(huì)將popupVisible設(shè)置為true然后顯示出portal,那么此時(shí)重點(diǎn)來了,我們這里實(shí)際上會(huì)有一個(gè)delay的延時(shí),也就是說實(shí)際上當(dāng)我們移出元素時(shí),在delay時(shí)間之后才會(huì)將元素真正的隱藏,那么如果此時(shí)我們將鼠標(biāo)再移入到portal,觸發(fā)onPopupMouseEnter事件時(shí)調(diào)用clearDelayTimer清除掉delayTimer,那么我們就可以阻止元素的隱藏,那么再往后的嵌套彈出層無論是child還是portal本身依舊是上一層portal的子元素,即使是在子portal與子child之間切換也可以利用clearDelayTimer來阻止元素的隱藏,所以之后的彈出層就可以利用這種方式遞歸處理就可以實(shí)現(xiàn)無限嵌套了。我們可以將DEMO中鼠標(biāo)從a -> b -> c -> d -> empty事件打印出來:

onMouseEnter a
onMouseLeave a
onPopupMouseEnter b
onMouseEnter c
onMouseLeave c
onPopupMouseLeave b
onPopupMouseEnter b
onPopupMouseEnter d
onPopupMouseLeave d
onPopupMouseLeave b

至此我們探究了Trigger組件的實(shí)現(xiàn),當(dāng)然在實(shí)際的處理過程中還有相當(dāng)多的細(xì)節(jié)需要處理,例如位置計(jì)算、動(dòng)畫、事件處理等等等等,而且實(shí)際上這個(gè)組件也有很多我們可以學(xué)習(xí)的地方,例如如何將外部傳遞的事件處理函數(shù)交予children、React.Children.mapReact.isValidElement、React.cloneElement等方法的使用等等,也都是非常有意思的實(shí)現(xiàn)。

const getWrappedChildren = () => {return React.Children.map(children, child => {if (React.isValidElement(child)) {const { props } = child;return React.cloneElement(child, {...props,onMouseEnter: mouseEnterHandler,onMouseLeave: mouseLeaveHandler,});} else {return child;}});
};

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://zhuanlan.zhihu.com/p/29880992
https://juejin.cn/post/6844904024378982413
https://juejin.cn/post/6904979968413925384
https://segmentfault.com/a/1190000012325351
https://zh-hans.legacy.reactjs.org/docs/portals.html
https://codesandbox.io/p/sandbox/trigger-component-1hv99o
https://zh-hans.react.dev/reference/react-dom/createPortal
https://github.com/arco-design/arco-design/blob/main/components/Trigger/index.tsx
http://www.risenshineclean.com/news/9246.html

相關(guān)文章:

  • 室內(nèi)設(shè)計(jì)培訓(xùn)教程seo實(shí)戰(zhàn)培訓(xùn)中心
  • 清爽css網(wǎng)站框架花關(guān)鍵詞排名系統(tǒng)
  • 小型電子商務(wù)網(wǎng)站開發(fā)百度愛采購?fù)茝V怎么入駐
  • 裝修行業(yè)門戶網(wǎng)站模板交換友情鏈接
  • 做學(xué)徒哪個(gè)網(wǎng)站好周口搜索引擎優(yōu)化
  • wap網(wǎng)站e4a做appseo上首頁排名
  • 科訊網(wǎng)站首頁公告模板吉林seo外包
  • 北京網(wǎng)站建設(shè)網(wǎng)絡(luò)公司5118和百度指數(shù)
  • php手機(jī)網(wǎng)站如何制作google怎么推廣
  • vs2019怎么創(chuàng)建網(wǎng)站每日一則小新聞
  • 騰訊云網(wǎng)站建設(shè)流程sem搜索引擎營銷
  • 寧波做網(wǎng)站價(jià)格網(wǎng)址域名
  • 網(wǎng)站建設(shè)合同內(nèi)容與結(jié)構(gòu)嗶哩嗶哩推廣網(wǎng)站
  • 鹽城網(wǎng)站app建設(shè)西安網(wǎng)站外包
  • 響應(yīng)式網(wǎng)站方案網(wǎng)站seo在線診斷
  • 大興網(wǎng)站開發(fā)網(wǎng)站建設(shè)哪家好谷歌搜索引擎入口
  • wordpress實(shí)訓(xùn)seo項(xiàng)目完整流程
  • 設(shè)計(jì)公司前十名aso排名優(yōu)化
  • 山東省兩學(xué)一做網(wǎng)站寫軟文怎么接單子
  • 網(wǎng)站做三屏合一百度一下電腦版網(wǎng)頁
  • 徐州睢寧網(wǎng)站建設(shè)seo教程搜索引擎優(yōu)化入門與進(jìn)階
  • 制作精美網(wǎng)站建設(shè)服務(wù)周到廣東東莞疫情最新情況
  • 女和男做搞基視頻網(wǎng)站國內(nèi)最新新聞事件
  • 龍巖建設(shè)局網(wǎng)站企業(yè)營銷策劃書如何編寫
  • 智慧團(tuán)建網(wǎng)站首頁網(wǎng)站推廣建站
  • 免費(fèi)的外鏈網(wǎng)站如何查看百度指數(shù)
  • 有沒有教做零食的網(wǎng)站百度推廣開戶怎么開
  • asp.net網(wǎng)站開發(fā)流程及相關(guān)工具2023近期輿情熱點(diǎn)事件
  • 創(chuàng)世網(wǎng)絡(luò)網(wǎng)站建設(shè)怎么樣太原百度公司地址
  • 剛備案的域名如何做網(wǎng)站營銷培訓(xùn)總結(jié)