ReactPortals传送门
ReactPortals传送门
React Portals
提供了一种将子节点渲染到父组件以外的DOM
节点的解决方案,即允许将JSX
作为children
渲染至DOM
的不同部分,最常见用例是子组件需要从视觉上脱离父容器,例如对话框、浮动工具栏、提示信息等。
描述
<div><SomeComponent />{createPortal(children, domNode, key?)}
</div>
React Portals
可以翻译为传送门,从字面意思上就可以理解为我们可以通过这个方法将我们的React
组件传送到任意指定的位置,可以将组件的输出渲染到DOM
树中的任意位置,而不仅仅是组件所在的DOM
层级内。举个简单的例子,假设我们ReactDOM.render
挂载组件的DOM
结构是<div id="root"></div>
,那么对于同一个组件我们是否使用Portal
在整个DOM
节点上得到的效果是不同的:
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`结构挂载到了`body`下 */}<div class="model"><div>456</div></div>
</body>
从上边的例子中可以看到我们通过ReactDOM.createPortal
将React
组件挂载到了其他的DOM
结构下,在这里是挂载到了document.body
下,当然这这也是最常见的做法,这样我们就可以通过Portal
将组件传送到目标渲染的位置,由此来更灵活地控制渲染的行为,并解决一些复杂的UI
交互场景,通常我们可以封装Portal
组件来更方便地调用。
export const Portal: React.FC = ({ children }) => {return typeof document === "object" ? ReactDOM.createPortal(children, document.body) : null;
};export const App: FC = () => (<Portal><SomeComponent /></Portal>
);
之前我们也聊到了,使用Portals
最常见的场景就是对话框,或者可以认为是浮动在整个页面顶部的组件,这样的组件在DOM
结构上是脱离了父组件的,我们当然可以自行实现相关的能力,例如主动创建一个div
结构挂载到目标DOM
结构下例如document.body
下,然后利用ReactDOM.render
将组建渲染到相关结构中,在组件卸载时再将创建的div
移除,这个方案当然是可行的但是并没有那么优雅。当然还有一个方法是使用状态管理,在目标组件中事先定义好相关的组件,通过状态管理例如redux
来控制显隐,这种就是纯粹的高射炮打蚊子,就没有必要再展开了。
其实我们再想一想,既然我们是要脱离父组件结构来实现这个能力,那么我们没有必要非得使用Portals
,CSS
的position
定位不是也可以帮助我们将当前的DOM
结构脱离文档流,也就是说我们没必要将目标组件的DOM
结构实际地分离出来,只需要借助position
定位就可以实现效果。当然想法是很美好的,真实场景就变得复杂的多了,那么脱离文档流最常用的主要是绝对定位absolute
与固定定位fixed
。首先我们来看一下absolute
,那么我们使用absolute
其实很容易想到,我们需要从当前组件一直到body
都没有其他position
是relative/absolute
的元素,这个条件肯定是很难达到的,特别是如果我们写的是一个组件库的话,很难控制用户究竟套了多少层以及究竟用什么CSS
属性。那么此时我们再将目光聚焦到fixed
上,fixed
是相对于视口来定位的,那么也就不需要像是absolute
那么强的要求了,即使是父元素存在relative/absolute
也没有关系。当然这件事没有这么简单,即使是fixed
元素依旧可能会受到父元素样式的影响,在这里举两个例子,分别是transform
与z-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`比父级高 也会被父级同级元素遮挡 -->
<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>
从上边的例子中我们可以看出,我们仅仅使用CSS
的position
定位是无法做到完全脱离父组件的,即使我们能够达到脱离文档流的效果,也会因为父组件的样式而受到影响,特别是在组件库中,我们作为第三方组件库的话是完全没有办法控制用户设计的DOM
结构的,如果仅仅采用脱离文档流的方法而不实际将DOM
结构分离出来的话,那么我们的组件就会受到用户样式的影响,这是我们不希望看到的。此外,即使我们并不是设计组件库,而仅仅是在我们的业务中实现相关需求,我们也不希望我们的组件受到父组件的影响,因为即使最开始我们的结构和样式没出现问题,随着业务越来越复杂,特别是多人协作开发项目,就很容易留下隐患,造成一些不必要的问题,当然我们可以引入E2E
来避免相关问题,这就是另一方面的解决方案了。
综上,React Portals
提供了一种更灵活地控制渲染的行为,可以用于解决一些复杂的UI
交互场景,下面是一些常见的应用场景:
- 模态框和对话框: 使用
Portals
可以将模态框或对话框组件渲染到DOM
树的顶层,确保其可以覆盖其他组件,并且在层级上独立于其他组件,这样可以避免CSS
或z-index
属性的复杂性,并且在组件层级之外创建一个干净的容器。 - 与第三方库的集成: 有时候,我们可能需要将
React
组件与第三方库(例如地图库或视频播放器)集成,使用Portals
可以将组件渲染到第三方库所需的DOM
元素中,即将业务需要的额外组件渲染到原组件封装好的DOM
结构中,以确保组件在正确的位置和上下文中运行。 - 逻辑分离和组件复用:
Portals
允许我们将组件的渲染输出与组件的逻辑分离,我们可以将组件的渲染输出定义在一个单独的Portal
组件中,并在需要的地方使用该Portal
,这样可以实现组件的复用,并且可以更好地组织和管理代码。 - 处理层叠上下文: 在某些情况下,使用
Portals
可以帮助我们解决层叠上下文stacking context
的问题,由于Portals
可以创建独立的DOM
渲染容器,因此可以避免由于层叠上下文导致的样式和布局问题。
MouseEnter事件
即使React Portals
可以将组件传送到任意的DOM
节点中,但是其行为和普通的React
组件一样,其并不会脱离原本的React
组件树,这其实是一件非常有意思的事情,因为这样会看起来,我们可以利用这个特性来实现比较复杂的交互。但是在这之前,我们来重新看一下MouseEnter
与MouseLeave
以及对应的MouseOver
与MouseOut
的原生DOM
事件。
MouseEnter
: 当鼠标光标进入一个元素时触发,该事件仅在鼠标从元素的外部进入时触发,不会对元素内部的子元素产生影响。例如,如果有一个嵌套的DOM
结构<div id="a"><div id="b"></div></div>
,此时我们在元素a
上绑定了MouseEnter
事件,当鼠标从该元素外部移动到内部时,MouseEnter
事件将被触发,而当我们再将鼠标移动到b
元素时,不会再次触发MouseEnter
事件。MouseLeave
:当鼠标光标离开一个元素时触发,该事件仅在鼠标从元素内部离开时触发,不会对元素外部的父元素产生影响。例如,如果有一个嵌套的DOM
结构<div id="a"><div id="b"></div></div>
,此时我们在元素a
上绑定了MouseEnter
事件,当鼠标从该元素内部移动到外部时,MouseLeave
事件将被触发,而如果此时我们的鼠标是从b
元素移出到a
元素内,不会触发MouseEnter
事件。MouseOver
: 当鼠标光标进入一个元素时触发,该事件在鼠标从元素的外部进入时触发,并且会冒泡到父元素。例如,如果有一个嵌套的DOM
结构<div id="a"><div id="b"></div></div>
,此时我们在元素a
上绑定了MouseOver
事件,当鼠标从该元素外部移动到内部时,MouseOver
事件将被触发,而当我们再将鼠标移动到b
元素时,由于冒泡会再次触发绑定在a
元素上的MouseOver
事件,再从b
元素移出到a
元素时会再次触发MouseOver
事件。MouseOut
: 当鼠标光标离开一个元素时触发,该事件在鼠标从元素内部离开时触发,并且会冒泡到父元素。例如,如果有一个嵌套的DOM
结构<div id="a"><div id="b"></div></div>
,此时我们在元素a
上绑定了MouseOut
事件,当鼠标从该元素内部移动到外部时,MouseOut
事件将被触发,而如果此时我们的鼠标是从b
元素移出到a
元素内,由于冒泡会同样触发绑定在MouseOut
事件,再从a
元素移出到外部时,同样会再次触发MouseOut
事件。
需要注意的是MouseEnter/MouseLeave
是在捕获阶段执行事件处理函数的,而不能在冒泡阶段过程中进行,而MouseOver/MouseOut
是可以在捕获阶段和冒泡阶段选择一个阶段来执行事件处理函数的,这个就看在addEventListener
如何处理了。实际上两种事件流都是可以阻断的,只不过MouseEnter/MouseLeave
需要在捕获阶段来stopPropagation
,一般情况下是不需要这么做的。我个人还是比较推荐使用MouseEnter/MouseLeave
,主要有这么几点理由:
- 避免冒泡问题:
MouseEnter
和MouseLeave
事件不会冒泡到父元素或其他元素,只在鼠标进入或离开元素本身时触发,这意味着我们可以更精确地控制事件的触发范围,更准确地处理鼠标交互,而不会受到其他元素的干扰,提供更好的用户体验。 - 避免重复触发:
MouseOver
和MouseOut
事件在鼠标悬停在元素内部时会重复触发,当鼠标从一个元素移动到其子元素时,MouseOut
事件会在父元素触发一次,然后在子元素触发一次,MouseOut
事件也是同样会多次触发,可以将父元素与所有子元素都看作独立区域,而事件会冒泡到父元素来执行事件绑定函数,这可能导致重复的事件处理和不必要的逻辑触发,而MouseEnter
和MouseLeave
事件不会重复触发,只在鼠标进入或离开元素时触发一次。 - 简化交互逻辑:
MouseEnter
和MouseLeave
事件的特性使得处理鼠标移入和移出的交互逻辑变得更直观和简化,我们可以仅关注元素本身的进入和离开,而不需要处理父元素或子元素的事件,这种简化有助于提高代码的可读性和可维护性。
当然究竟使用MouseEnter/MouseLeave
还是MouseEnter/MouseLeave
事件还是要看具体的业务场景,如果需要处理鼠标移入和移出元素的子元素时或者需要利用冒泡机制来实现功能,那么MouseOver
和MouseOut
事件就是更好的选择,MouseEnter/MouseLeave
能提供更大的灵活性和控制力,让我们能够创建复杂的交互效果,并更好地处理用户与元素的交互,当然应用的复杂性也会相应提高。
让我们回到MouseEnter/MouseLeave
事件本身上,在这里https://codesandbox.io/p/sandbox/trigger-component-1hv99o?file=/src/components/mouse-enter-test.tsx:1,1
提供了一个事件的DEMO
可以用来测试事件效果。需要注意的是,在这里我们是借助于React
的合成事件来测试的,而在测试的时候也可以比较明显地发现MouseEnter/MouseLeave
的TS
提示是没有Capture
这个选项的,例如Click
事件是有onClick
与onClickCapture
来表示冒泡和捕获阶段事件绑定的,而即使是在React
合成事件中MouseEnter/MouseLeave
也只会在捕获阶段执行,所以没有Capture
事件绑定属性。
--------------------------
| c | b | a |
| | | |
|------- | |
| | |
|---------------- |
| |
--------------------------
我们分别在三个DOM
上都绑定了MouseEnter
事件,当我们鼠标移动到a
上时,会执行a
元素绑定的事件,当依次将鼠标移动到a
、b
、c
的时候,同样会以此执行a
、b
、c
的事件绑定函数,并且不会因为冒泡事件导致父元素事件的触发,当我们鼠标直接移动到c
的时候,可以看到依旧是按照a
、b
、c
的顺序执行,也可以看出来MouseEnter
事件是依赖于捕获阶段执行的。
Portal事件
在前边也提到了,尽管React Portals
可以被放置在DOM
树中的任何地方,但在任何其他方面,其行为和普通的React
子节点行为一致。我们都知道React
自行维护了一套基于事件代理的合成事件,那么由于Portal
仍存在于原本的React
组件树中,这样就意味着我们的React
事件实际上还是遵循原本的合成事件规则而与DOM
树中的位置无关,那么我们就可以认为其无论其子节点是否是Portal
,像合成事件、Context
这样的功能特性都是不变的,下面是一些使用React Portals
需要关注的点:
- 事件冒泡会正常工作: 合成事件将通过冒泡传播到
React
树的祖先,事件冒泡将按预期工作,而与DOM
中的Portal
节点位置无关。 React
以控制Portal
节点及其生命周期:Portal
未脱离React
组件树,当通过Portal
渲染子组件时,React
仍然可以控制组件的生命周期。Portal
只影响DOM
结构: 对于React
来说Portal
仅仅是视觉上渲染的位置变了,只会影响HTML
的DOM
结构,而不会影响React
组件树。- 预定义的
HTML
挂载点: 使用React Portal
时,我们需要提前定义一个HTML DOM
元素作为Portal
组件的挂载。
在这里https://codesandbox.io/p/sandbox/trigger-component-1hv99o?file=/src/components/portal-test.tsx:1,1
提供了一个Portals
与MouseEnter
事件的DEMO
可以用来测试效果。那么在代码中实现的嵌套精简如下:
-------------------
| 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>
);
单纯从代码上来看,这就是一个很简单的嵌套结构,而因为传送门Portals
的存在,在真实的DOM
结构上,这段代码结构表现的效果是这样的,其中id
只是用来标识React
的DOM
结构,实际并不存在:
<body><div id="root"><div id="a"></div></div><div id="b"></div><div id="c"></div><div>
</body>
接下来我们依次来试试定义的MouseEnter
事件触发情况,首先鼠标移动到a
元素上,控制台打印a
,符合预期,接下来鼠标移动到b
元素上,控制台打印b
,同样符合预期,那么接下来将鼠标移动到c
,神奇的事情来了,我们会发现会先打印b
再打印c
,而不是仅仅打印了c
,由此我们可以得到虽然看起来DOM
结构不一样了,但是在React
树中合成事件依然保持着嵌套结构,C
组件作为B
组件的子元素,在事件捕获时依然会从B -> C
触发MouseEnter
事件,基于此我们可以实现非常有意思的一件事情,多级嵌套的弹出层。
Trigger弹出层
实际上上边聊的内容都是都是为这部分内容做铺垫的,因为工作的关系我使用ArcoDesign
是非常多的,又由于我实际是做富文本文档的,需要弹出层来做交互的地方就非常多,所以在平时的工作中会大量使用ArcoDesign
的Trigger
组件https://arco.design/react/components/trigger
,之前我一直非常好奇这个组件的实现,这个组件可以无限层级地嵌套,而且当多级弹出层组件的最后一级鼠标移出之后,所有的弹出层都会被关闭,最主要的是我们只是将其嵌套做了一层业务实现,并没有做任何的通信传递,所以我也一直好奇这部分的实现,直到前一段时间我为了解决BUG
深入研究了一下相关实现,发现其本质还是利用React Portals
以及React
树的合成事件来完成的,这其中还是有很多交互实现可以好好学习下的。
同样的,在这里也完成了一个DEMO
实现https://codesandbox.io/p/sandbox/trigger-component-1hv99o?file=/src/components/trigger-simple.tsx:1,1
,而在调用时,则直接嵌套即可实现两层弹出层,当我们鼠标移动到a
元素时,b
元素与c
元素会展示出来,当我们将鼠标移动到c
元素时,d
元素会被展示出来,当我们继续将鼠标快速移动到d
元素时,所有的弹出层都不会消失,当我们直接将鼠标从d
元素移动到空白区域时,所有的弹出层都会消失,如果我们将其移动到b
元素,那么只有d
元素会消失。
------------------- ------------- --------
| 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>
让我们来拆解一下代码实现,首先是Portal
组件的封装,在这里我们就认为我们将要挂载的组件是在document.body
上的就可以了,因为我们要做的是弹出层,在最开始的时候也阐明了我们的弹出层DOM
结构需要挂在最外层而不能直接嵌套地放在DOM
结构中,当然如果能够保证不会出现相关问题,滚动容器不是body
的情况且需要position absolute
的情况下,可以通过getContainer
传入DOM
节点来制定传送的位置,当然在这里我们认为是body
就可以了。在下面这段实现中我们就通过封装Portal
组件来调度DOM
节点的挂载和卸载,并且实际的组件也会被挂载到我们刚创建的节点上。
// 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;
};
接下来我们来看构造在React
树中的DOM
结构,这块可以说是整个实现的精髓,可能会比较绕,可以认为实际上每个弹出层都分为了两块,一个是原本的child
,另一个是弹出的portal
,这两个结构是平行的放在React DOM
树中的,那么在多级弹出层之后,实际上每个子trigger(portal + child)
都是上层portal
的children
,这个结构可以用一个树形结构来表示。
<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>
从树形结构中我们可以看出来,虽然在DOM
结构中我们现实出来是平铺的结构,但是在React
的事件树中却依旧保持着嵌套结构,那么我们就很容易解答最开始的一个问题,为什么我们可以无限层级地嵌套,而且当多级弹出层组件的最后一级鼠标移出之后,所有的弹出层都会被关闭,就是因为实际上即使我们的鼠标在最后一级,但是在React
树结构中其依旧是属于所有portal
的子元素,既然其是child
那么实际上我们可以认为其并没有移出各级trigger
的元素,自然不会触发MouseLeave
事件来关闭弹出层,如果我们移出了最后一级弹出层到空白区域,那么相当于我们移出了所有trigger
实例的portal
元素区域,自然会触发所有绑定的MouseLeave
事件来关闭弹出层。
那么虽然上边我们虽然解释了Trigger
组件为什么能够维持无限嵌套层级结构下能够维持弹出层的显示,并且在最后一级鼠标移出之后能够关闭所有弹出层,或者从最后一级返回到上一级只关闭最后一级弹出层,但是我们还有一个问题没有想明白,上边的问题是因为所有的trigger
弹出层实例都是上一级trigger
弹出层实例的子元素,那么我们还有一个平级的portal
与child
元素呢,当我们鼠标移动到child
时,portal
元素会展示出来,而此时我们将鼠标移动到portal
元素时,这个portal
元素并不会消失,而是会一直保持显示,在这里的React
树是不存在嵌套结构的,所以这里需要对事件进行特殊处理。
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();}
};
实际上在这里的通信会比较简单,之前我们也提到portal
与child
元素是平级的,那么我们可以明显地看出来实际上这是在一个组件内的,那么整体的实现就会简单很多,我们可以设计一个延时,并且可以为portal
和child
分别绑定MouseEnter
和MouseLeave
事件,在这里我们为child
绑定的是onMouseEnter
和onMouseLeave
两个事件处理函数,为portal
绑定了onPopupMouseEnter
和onPopupMouseLeave
两个事件处理函数。那么此时我们模拟一下上边的情况,当我们鼠标移入child
元素时,会触发onMouseEnter
事件处理函数,此时我们会清除掉delayTimer
,然后会调用setPopupVisible
方法,此时会将popupVisible
设置为true
然后显示出portal
,那么此时重点来了,我们这里实际上会有一个delay
的延时,也就是说实际上当我们移出元素时,在delay
时间之后才会将元素真正的隐藏,那么如果此时我们将鼠标再移入到portal
,触发onPopupMouseEnter
事件时调用clearDelayTimer
清除掉delayTimer
,那么我们就可以阻止元素的隐藏,那么再往后的嵌套弹出层无论是child
还是portal
本身依旧是上一层portal
的子元素,即使是在子portal
与子child
之间切换也可以利用clearDelayTimer
来阻止元素的隐藏,所以之后的弹出层就可以利用这种方式递归处理就可以实现无限嵌套了。我们可以将DEMO
中鼠标从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
组件的实现,当然在实际的处理过程中还有相当多的细节需要处理,例如位置计算、动画、事件处理等等等等,而且实际上这个组件也有很多我们可以学习的地方,例如如何将外部传递的事件处理函数交予children
、React.Children.map
、React.isValidElement
、React.cloneElement
等方法的使用等等,也都是非常有意思的实现。
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
相关文章:
ReactPortals传送门
ReactPortals传送门 React Portals提供了一种将子节点渲染到父组件以外的DOM节点的解决方案,即允许将JSX作为children渲染至DOM的不同部分,最常见用例是子组件需要从视觉上脱离父容器,例如对话框、浮动工具栏、提示信息等。 描述 <div&…...
【GDB】 command 命令
GDB command 命令 语法 command 命令是一个很好用的调试命令,它配合断点使用,可以在指定的断点执行预先设置的命令 其语法为:command bread_id,这样会提示你输入你要执行的命令,以 end 结束。这个 bread_id 就是用 …...
1038 统计同成绩学生
输入样例: 10 60 75 90 55 75 99 82 90 75 50 3 75 90 88 输出样例: 3 2 0 solution #include <stdio.h> int main(){int n, d, k, hash[101] {0}, a[100000];scanf("%d", &n);for(int i 0; i < n; i){scanf("%d&quo…...
git报错:Failed to connect to 127.0.0.1 port 1080
Bug描述 由于在试了网上的这条命令 git config --global http.proxy socks5 127.0.0.1:1080 git config --global https.proxy socks5 127.0.0.1:1080git config --global http.proxy 127.0.0.1:1080 git config --global https.proxy 127.0.0.1:1080Bug描述:Faile…...
php eayswoole node axios crypto-js 实现大文件分片上传复盘
不啰嗦 直接上步骤 步骤1.开发环境配置 项目需要node.js 做前端支撑 官网下载地址: http://nodejs.cn/download/ 根据自己需要下载对应的版本,我下载的是windows系统64位的版本。 包下载好后 进行安装,安装步骤在此省略... 测试是否安装成功 …...
《Upload-Labs》01. Pass 1~13
Upload-Labs 索引前言Pass-01题解 Pass-02题解总结 Pass-03题解总结 Pass-04题解 Pass-05题解总结 Pass-06题解总结 Pass-07题解总结 Pass-08题解总结 Pass-09题解 Pass-10题解 Pass-11题解 Pass-12题解总结 Pass-13题解 靶场部署在 VMware - Win7。 靶场地址:https…...
v-for中的key
在Vue中,当使用v-for指令循环渲染元素时,添加:key是一个推荐做法,尤其是在循环的元素可能会被重新排序、添加或删除的情况下。 :key的作用是为每个循环的元素提供一个唯一的标识符,以便Vue能够跟踪和管理这些元素的状态。Vue使用…...
MySQL学习笔记17
MySQL权限管理grant: 权限说明: Table 6.2 Permissible Privileges for GRANT and REVOKE PrivilegeGrant Table ColumnContextALL [PRIVILEGES]Synonym for “all privileges”Server administrationALTERAlter_privTablesALTER ROUTINEAlter_routin…...
跨境电商建站:选择域名需要注意什么?
在跨境电商建站过程中,选择一个合适的域名至关重要,尤其是对于跨境电商独立站来说,它对未来的seo排名和品牌建设都有着重要影响。关于本文,我会先从域名的定义开始,到域名选择的重要性,再到如何选择一个完美…...
jupyterlab
1. 环境:linux 环境(基于ubuntu-fork 镜像实现) 2. pip install jupyter notebook 3. 编译jupyterlab源代码必须使用 node 14.21.3 高版本编译报错#下载 node 14.21.3 :wget https://nodejs.org/download/release/latest-v14.x/node-v14.21.…...
Oracle的递归公共表表达式
查询节点id为2的所有子节点的数据,包括向下级联 WITH T1 (id, parent_id, data) AS (SELECT id, parent_id, dataFROM nodesWHERE id 2UNION ALLSELECT t.id, t.parent_id, t.dataFROM nodes tJOIN T1 n ON t.parent_id n.id ) SELECT * FROM T1; --建表语句 C…...
解决antd vue ts v-model:value绑定Boolean布尔类型爆红但可以使用
没啥好写的,写点注意点把 前言:在 antd vue中需要用到对应的类型转换,v-model后面补一个value来表明类型,但这也是默认类型,并不是指定类型默认是 (property) value?: string | number | undefined 字符,…...
zblog插件大全-zblog免费插件
在当今数字化时代,拥有一个精彩的博客或网站已经成为许多人追求的目标。通过博客,我们可以分享知识、表达观点,甚至创造收入。然而,维持一个充满新鲜内容的博客却不是一件容易的事情。 ZBlog自动采集插件 什么是ZBlog自动采集插件…...
思科、华为、华三、锐捷网络设备巡检命令
下面为四种设备巡检命令,以便日常查阅: 华三 screen-length disable 取消分页 displayversion 查看版本 display clock 查看日期时钟 display fan 查看风扇状态 display power 查看电源信息 display cpu-usage 查看CPU利用率 display memory 查看…...
正则表达式贪婪模式和非贪婪模式
一、贪婪模式 贪婪模式表示尽可能多的匹配字符串,正则表达式六个量词元字符?、、*、{n}、{n,m}、{n,}默认是贪婪模式 接下来引入一个场景来分析说明 获取html a标签href属性的值 <a href"https://www.baidu.com/" attr"abc"></a>…...
借助 ControlNet 生成艺术二维码 – 基于 Stable Diffusion 的 AI 绘画方案
背景介绍 在过去的数月中,亚马逊云科技已经推出了多篇博文,来介绍如何在亚马逊云科技上部署 Stable Diffusion,或是如何结合 Amazon SageMaker 与 Stable Diffusion 进行模型训练和推理任务。 为了帮助客户快速、安全地在亚马逊云科技上构建、…...
Codeforces Round 892 (Div. 2) - E. Maximum Monogonosity 思维dp 详细解析
题目链接 好久没有写题了复健一下qwq 题目大意 解题思路 这题目还挺妙的 首先考虑比较正常的dp, d p [ i ] [ j ] dp[i][j] dp[i][j] 为前 i i i的长度选 j j j个长度的最大价值,那么转移方程是: 图片来自:图片来源 但是这个是 …...
R语言中的数据重塑
文章目录 介绍reshape2::melt()的用法实例 reshape2::dcast()的用法实例 tidyr::gather()的用法tidyr::spread()的用法 介绍 tidyverse系列包中的函数操作都是针对简洁数据框进行的,对于不是简洁的数据,实现需要进行数据重塑。数据重塑主要包括长宽表的…...
基于Java实现的社区团购系统设计与实现(源码+lw+部署文档+讲解等)
文章目录 前言系统功能具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序(小蔡coding)有保障的售后福利 代码参考源码获取 前言 💗博主介绍:✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域…...
nodejs+vue网上婚纱购物系统elementui
便了用户足不出门也能进行购物的理念,方便了婚纱影楼的对商品的进一步管理,互联网成为人们快速获取、发布、和传递信息的重要渠道,它在人们政治、经济、生活等各个方面发挥着重要的作用。未来的时代是网络信息的时代,“网上生活方式”是人类今…...
【2023集创赛】加速科技杯三等奖作品:私密性高精度刷手身份认证系统
本文为2023年第七届全国大学生集成电路创新创业大赛(“集创赛”)加速科技杯三等奖作品分享,参加极术社区的【有奖征集】分享你的2023集创赛作品,秀出作品风采,分享2023集创赛作品扩大影响力,更有丰富电子礼…...
1500*C. Kefa and Park(dfstree)
Kefa and Park - 洛谷 Problem - 580C - Codeforces Examples input 4 1 1 1 0 0 1 2 1 3 1 4 output 2 input 7 1 1 0 1 1 0 0 0 1 2 1 3 2 4 2 5 3 6 3 7 output 2 解析: dfs遍历,记录前一个结点权值是否为1,并且累计路径1的个数…...
【2023保研】双非上岸东南网安
个人情况 学校:henu 专业:信息安全 排名:1/66 英语:六级500 竞赛:蓝桥杯PB国一,ISCC国一,密码数学挑战赛国三,还有其他一些省级水奖 论文:一篇EI在投(三作通…...
Redis与Mybatis
作者在学习Redis整合时使用JDBC与Jedis,但是呢,现如今的环境下,Mybatis系列ORM框架是更受关注的方法,作者有一点点Mybatis基础,Mybatisplus几乎忘的差不多了,现对Redis整合Mybatis相关知识进行梳理…...
MySQL架构 InnoDB存储引擎
1. 什么是Mysql? 我们在开发的时候,我们都需要对业务数据进行存储,这个时候,你们就会用到MySQL、Oracal等数据库。 MySQL它是一个关系型数据库,这种关系型数据库就有Oracal、 MySQL,以及最近很火的PgSQL等。…...
K8S-CNI
CNI的设计思想即为:Kubernetes在启动Pod的pause容器之后,直接调用CNI网络插件,从而实现为Pod内部应用容器月在的Network Namespace配置符合预期的网络信息。 这里面需要特别关注两个方面:Container必须有自己的网络命名空间的环境,也就是end…...
Redis 集合类型(Set)和命令 (数据类型 四)
集合类型是一个无序、不重复的数据集合,它可以用于存储唯一的值,并提供了对集合进行交集、并集、差集等操作。 常用集合类型命令: 添加操作: sadd key member1 member2 …:向集合中添加一个或多个成员。 # 添加三个…...
thinkphp5 如何模拟在apifox里面 post数据接收
tp5里面控制器写的方法想直接apifox里面请求接受 必须带上这个参数 header里面 X-Requested-With:XMLHttpRequest...
建造者模式 创建型模式之三
想要搞清楚建造者模式,首先先要了解建造者模式种四个角色的定位 1.Product:表示被构造的复杂对象,就是我们要建造的东西,比如我们要做一个手机,手机就是product。 2.Builder:建造者,这里需要着…...
发布以太坊测试网络中的第一笔交易
1.安装以太坊钱包 要想发送发布以太坊测试网络中的第一笔交易,首先需要创建一个管理账户的钱包,这个钱包可以理解为管理私钥的容器,具体按照步骤为:打开Chrome浏览器应用商店搜索MetaMask,选择对应的钱包添加至Chrome…...
吴兴网站建设/市场调研报告的基本框架
基于单片机的智能豆浆机控制系统设计(附实物图,电路原理图,程序)(任务书,开题报告,中期检查表,毕业论文11800字)摘要由于市面上的豆浆机不具有长时间保温的功能,打浆完成后残渣很多,而且打浆用时较长。因此,本次设计制作了基于51单片机的全自…...
如何做垂直门户网站/今日头条热搜榜
1、firewall相关的操作 查看防火墙状态: firewall-cmd --state 关闭防火墙 systemctl stop firewalld.service 开启防火墙 systemctl start firewalld.service 禁止开机启动启动防火墙 systemctl disable firewalld.service 2、iptables相关的…...
wordpress xstore/徐州seo管理
这是一款jquery全屏响应式淡入淡出效果轮播图插件。该轮播图插件简单实用,兼容ie8浏览器,可以自动切换播放,带淡入淡出的动画效果。使用方法在页面中引入jquery和jquery.flexslider-min.js文件。HTML结构实用一个作为轮播图的容器,…...
上海网站制作电话/软文广告经典案例
PyTorch 自定义序列数据集 flyfish 实现 主要是DataLoader, TensorDataset。 数据集代码与模型训练代码解耦,以获得更好的可读性和模块化。PyTorch提供了两个数据原语:torch.utils.data.DataLoader和torch.utils.data.Dataset, 允许使用预…...
网站兼容9/有没有可以代理推广的平台
如今互联网技术在不断地发展和进步。同时Web3D产品建模技术也越来越承受,被广泛应用于诸多行业当中。商迪3D构建的Web3D产品建模可视化三维模型在产品建模和宣传展示中,Web3D技术发挥了极大的作用,并为产品建模和展示注入了新的活力。电商产品…...
东阳科技网站建设/网络营销和市场营销的区别
通过昨天晚上的研究,让我对group by等函数感到很多的不解。对于其的用法感到很困惑。学而不思则罔,思而不学则殆。动手研究,今天先进行一点。 SQL> select * from emp 2 ; EMPNO ENAME JOB MGR HIREDATE SAL …...