一、组件介绍
element-plus的官方文档中没有ElPopper组件,ElPopper在其他组件内部使用,如:Select、Cascade、ColorPicker等组件中都用到了ElPopper;
官方文档中提供的弹框组件是ElPopover,这两个组件之间的关系非常密切,这里先分析ElPopper组件。
二、源码分析
2.1 popper.ts
ElPopper组件及其子组件都使用render函数编写。
popper.ts对应弹出内容
// packages\components\popper\src\renderers\popper.ts
// 参数:
// props: 传入的属性
// children: 子元素数组
export default function renderPopper(
props: IRenderPopperProps,
children: VNode[],
) {
// 从props中解构出对应属性
const {
effect,
name,
stopPopperMouseEvent,
popperClass,
popperStyle,
popperRef,
pure,
popperId,
visibility,
onMouseenter,
onMouseleave,
onAfterEnter,
onAfterLeave,
onBeforeEnter,
onBeforeLeave,
} = props
// 动态class,缩写挺逗的
const kls = [
popperClass,
'el-popper',
'is-' + effect,
pure ? 'is-pure' : '',
]
// 鼠标按下/松开事件 stop = (e: Event) => e.stopPropagation()
const mouseUpAndDown = stopPopperMouseEvent ? stop : NOOP
// render函数
return h(
// 外层组件,使用vue官方过渡组件
Transition,
// 属性,主要是过渡事件
{
name,
'onAfterEnter': onAfterEnter,
'onAfterLeave': onAfterLeave,
'onBeforeEnter': onBeforeEnter,
'onBeforeLeave': onBeforeLeave,
},
// slots
{
// 默认插槽
// withCtx withDirectives 是vue官方内部方法,
default: withCtx(() => [withDirectives(
// 内部元素,部分传入的属性绑定在这一层元素上
h(
'div',
{
'aria-hidden': String(!visibility),
class: kls,
style: popperStyle ?? {},
id: popperId,
ref: popperRef ?? 'popperRef',
role: 'tooltip',
onMouseenter,
onMouseleave,
onClick: stop,
onMousedown: mouseUpAndDown,
onMouseup: mouseUpAndDown,
},
children,
),
// 指令,等同于 v-show="visibility"
[[vShow, visibility]],
)]),
},
)
/**
* 以上render函数,等同于:
* <transition :name="name">
* <div v-show="visibility" :aria-hidden="!visibility" :class="kls" ref="popperRef" role="tooltip" @mouseenter="" @mouseleave="" @click="">
* <slot />
* </div>
* </transition>
*/
}
总结:
popper.ts提供了renderPopper方法,该方法接收2个参数:传入的属性及子元素数组;renderPopper方法中,外层使用Transition组件展示过渡效果,内层使用div标签,将相应属性绑定在这一层div标签上,传入的children子元素作为这一层div的子元素。
2.2 trigger.ts
trigger.ts对应触发部分
// packages\components\popper\src\renderers\trigger.ts
interface IRenderTriggerProps extends Record<string, unknown> {
ref: string | Ref<ComponentPublicInstance | HTMLElement>
onClick?: EventHandler
onMouseover?: EventHandler
onMouseleave?: EventHandler
onFocus?: EventHandler
}
// 参数:
// 1、元素数组 2、传入的属性
export default function renderTrigger(trigger: VNode[], extraProps: IRenderTriggerProps) {
// 获取第一个有效的VNode节点
const firstElement = getFirstValidNode(trigger, 1)
// 没有有效VNode节点则报错提示
if (!firstElement) throwError('renderTrigger', 'trigger expects single rooted node')
// 使用vue的cloneVNode api,将extraProps和firstElement的props融合
return cloneVNode(firstElement, extraProps, true)
}
总结:
trigger.ts的逻辑比较简单,找到第一个有效的VNode节点,并且将一些额外的属性融入到VNode节点中。
2.3 arrow.ts
arrow.ts是弹框的箭头部分
// packages\components\popper\src\renderers\arrow.ts
// 参数:1、是否展示箭头
export default function renderArrow(showArrow: boolean) {
// 根据showArrow决定是否展示箭头
return showArrow
? h(
'div',
{
ref: 'arrowRef',
class: 'el-popper__arrow',
'data-popper-arrow': '',
},
null,
)
: h(Comment, null, '')
}
arrow.ts的逻辑更加简单,根据传入的showArrow参数决定是否展示箭头。
2.4 index.vue
前面介绍了ElPopper的3个子组件部分,现在来看一下组件本身的index.vue
// packages\components\popper\src\index.vue
export default defineComponent({
name: compName,
props: defaultProps,
emits: [UPDATE_VISIBLE_EVENT, 'after-enter', 'after-leave', 'before-enter', 'before-leave'],
setup(props, ctx) {
// 如果用户没有写trigger插槽,则报错提示:必须提供trigger
if (!ctx.slots.trigger) {
throwError(compName, 'Trigger must be provided')
}
// 调用usePopper,返回值是popper的状态数据
const popperStates = usePopper(props, ctx)
// 封装强制销毁方法
const forceDestroy = () => popperStates.doDestroy(true)
// 挂载时调用initializePopper
onMounted(popperStates.initializePopper)
// 卸载时销毁
onBeforeUnmount(forceDestroy)
// keep-alive激活时调用initializePopper
onActivated(popperStates.initializePopper)
// 失效时销毁
onDeactivated(forceDestroy)
// 返回状态数据
return popperStates
},
render() {
// 从this上解构属性,其中一部分属性是setup函数返回的,一部分是vue组件初始化时生成的
const {
$slots,
appendToBody,
class: kls,
style,
effect,
hide,
onPopperMouseEnter,
onPopperMouseLeave,
onAfterEnter,
onAfterLeave,
onBeforeEnter,
onBeforeLeave,
popperClass,
popperId,
popperStyle,
pure,
showArrow,
transition,
visibility,
stopPopperMouseEvent,
} = this
// 是否手动触发模式
const isManual = this.isManualMode()
// 箭头
const arrow = renderArrow(showArrow)
// 弹框
const popper = renderPopper(
// 属性
{
effect,
name: transition,
popperClass,
popperId,
popperStyle,
pure,
stopPopperMouseEvent,
onMouseenter: onPopperMouseEnter,
onMouseleave: onPopperMouseLeave,
onAfterEnter,
onAfterLeave,
onBeforeEnter,
onBeforeLeave,
visibility,
},
// children
[
// 默认插槽内容
renderSlot($slots, 'default', {}, () => {
return [toDisplayString(this.content)]
}),
// 箭头
arrow,
],
)
// trigger slot
const _t = $slots.trigger?.()
// trigger props
const triggerProps = {
'aria-describedby': popperId,
class: kls,
style,
ref: 'triggerRef',
...this.events,
}
// 根据是否手动触发,决定是否给trigger添加clickOutside指令
const trigger = isManual
? renderTrigger(_t, triggerProps)
: withDirectives(renderTrigger(_t, triggerProps), [[ClickOutside, hide]])
// Fragment是vue内置的片段,其不会渲染出任何节点
return h(Fragment, null,
// children
[
// 触发元素
trigger,
// Teleport是vue内置的传送门组件,将子元素渲染到to指定的元素中
// 这里的to赋值为body,即弹框部分挂载到body下
h(
Teleport as any, // Vue did not support createVNode for Teleport
{
to: 'body',
// 若appendToBody值为false,则禁用传送门
disabled: !appendToBody,
},
// 弹框内容
[popper],
),
])
},
})
总结:
- 在
setup方法中调用usePopper方法生成popper的状态数据;并注册生命周期钩子函数,在挂载/激活时调用initializePopper方法,在卸载/失效时调用doDestroy(true); - 在
render函数中,首先通过解构赋值的方式从this上获取相应的属性值;调用renderArrow/renderPopper/renderTrigger生成相应子模块,调用时传入相应模块所需的属性值; - 使用
Teleport传送门组件,将popper部分挂载到body上
2.5 use-popper/index.ts
在上面的index.vue中,其setup函数内调用了userPopper方法,我们进一步分析一下这个方法:
// packages\components\popper\src\use-popper\index.ts
export default function(
props: IPopperOptions,
{ emit }: SetupContext<EmitType[]>,
) {
// arrow trigger popper三个部分的ref
const arrowRef = ref<RefElement>(null)
const triggerRef = ref(null) as Ref<ElementType>
const popperRef = ref<RefElement>(null)
// 生成唯一的随机id
const popperId = `el-popper-${generateId()}`
let popperInstance: Nullable<PopperInstance> = null
// 延迟出现的计时器,即触发显示后,延迟一段时间再显示
let showTimer: Nullable<TimeoutHandle> = null
// 自动隐藏计时器,即显示弹框后,一段时间后自动隐藏
let hideTimer: Nullable<TimeoutHandle> = null
// trigger的focus状态
let triggerFocused = false
// 是否手动触发模式
const isManualMode = () => props.manualMode || props.trigger === 'manual'
// popper的动态样式,这里是通过PopupManager生成一个zIndex值
const popperStyle = ref<CSSProperties>({ zIndex: PopupManager.nextZIndex() })
// 调用usePopperOptions生成popperOptions
const popperOptions = usePopperOptions(props, {
arrow: arrowRef,
})
// 响应式数据,将props.visible的值转换成boolean类型,
const state = reactive({
visible: !!props.visible,
})
// visible has been taken by props.visible, avoiding name collision
// Either marking type here or setter parameter
// 计算属性visibility,有get和set方法
const visibility = computed<boolean>({
get() {
if (props.disabled) {
return false
} else {
return isBool(props.visible) ? props.visible : state.visible
}
},
set(val) {
if (isManualMode()) return
isBool(props.visible)
? emit(UPDATE_VISIBLE_EVENT, val)
: (state.visible = val)
},
})
// 内部的show方法
function _show() {
// autoClose用户控制弹框出现后,自动隐藏的延迟,若为0则不会自动隐藏
if (props.autoClose > 0) {
// 定时器,时间到时调用_hide隐藏
hideTimer = window.setTimeout(() => {
_hide()
}, props.autoClose)
}
visibility.value = true
}
// 内部的hide方法
function _hide() {
visibility.value = false
}
// 清除计时器,包括showTimer和hideTimer
function clearTimers() {
clearTimeout(showTimer)
clearTimeout(hideTimer)
}
// 展示
const show = () => {
if (isManualMode() || props.disabled) return
// 清除计时器
clearTimers()
if (props.showAfter === 0) {
_show()
} else {
// 有延迟显示时,使用计时器
showTimer = window.setTimeout(() => {
_show()
}, props.showAfter)
}
}
// 隐藏
const hide = () => {
if (isManualMode()) return
// 清除计时器
clearTimers()
if (props.hideAfter > 0) {
// 若有延迟关闭,则使用计时器
hideTimer = window.setTimeout(() => {
close()
}, props.hideAfter)
} else {
close()
}
}
// 关闭
const close = () => {
_hide()
if (props.disabled) {
// 如果禁用,调用销毁
doDestroy(true)
}
}
// 鼠标进入popper事件处理函数
function onPopperMouseEnter() {
// trigger非click模式下,鼠标进入popper,清除自动隐藏计时器
if (props.enterable && props.trigger !== 'click') {
clearTimeout(hideTimer)
}
}
// 鼠标离开popper事件处理函数
function onPopperMouseLeave() {
const { trigger } = props
const shouldPrevent =
(isString(trigger) && (trigger === 'click' || trigger === 'focus')) ||
(trigger.length === 1 &&
(trigger[0] === 'click' || trigger[0] === 'focus'))
// trigger是click或focus时,不触发关闭弹框
if (shouldPrevent) return
// trigger是hover时关闭弹框,因为hide方法中对manual模式进行了判断,因此只有hover模式下会关闭
hide()
}
// 初始化 popper
function initializePopper() {
// $()方法是element-plus自定义的一个方法,作用是直接拿到ref类型数据的value值
// function $<T>(ref: Ref<T>) {
// return ref.value
// }
if (!$(visibility)) {
// 隐藏状态下,直接return
return
}
const unwrappedTrigger = $(triggerRef)
// 判断trigger元素是HTML原生元素还是vue组件
// 如果是组件,取其$el
const _trigger = isHTMLElement(unwrappedTrigger)
? unwrappedTrigger
: (unwrappedTrigger as ComponentPublicInstance).$el
// 关键点:createPopper是popperjs库提供的方法
// createPopper接收3个参数:1、reference,即触发弹框的对象;2、popper: 即弹框元素;3、popperOptions:即弹框配置
popperInstance = createPopper(_trigger, $(popperRef), $(popperOptions))
// 调用update
popperInstance.update()
}
// 销毁
function doDestroy(forceDestroy?: boolean) {
/* istanbul ignore if */
if (!popperInstance || ($(visibility) && !forceDestroy)) return
detachPopper()
}
function detachPopper() {
// 调用popperjs的destroy方法
popperInstance?.destroy?.()
popperInstance = null
}
const events = {} as PopperEvents
// 显示切换副作用函数
function onVisibilityChange(toState: boolean) {
if (toState) {
// 变成可见状态时,更新zIndex
popperStyle.value.zIndex = PopupManager.nextZIndex()
// 重新调用initializePopper
initializePopper()
}
}
// 显示切换时,调用显示切换副作用函数
watch(visibility, onVisibilityChange)
// 非自动触发模式下
if (!isManualMode()) {
// 定义切换显示方法
const toggleState = () => {
if ($(visibility)) {
hide()
} else {
show()
}
}
// 定义事件处理函数
const popperEventsHandler = (e: Event) => {
// 阻止冒泡
e.stopPropagation()
switch (e.type) {
// 点击事件
case 'click': {
if (triggerFocused) {
// 当前focus,则调整为非focus
triggerFocused = false
} else {
// 当前非focus,则切换显示状态
toggleState()
}
break
}
// 鼠标进入事件
case 'mouseenter': {
// 显示
show()
break
}
// 鼠标移出事件
case 'mouseleave': {
// 隐藏
hide()
break
}
// focus事件
case 'focus': {
// 置为focus状态,并显示
triggerFocused = true
show()
break
}
// 失焦事件
case 'blur': {
// 置为非focus状态,并隐藏
triggerFocused = false
hide()
break
}
}
}
// 根据trigger类型,绑定对应事件
const triggerEventsMap: Partial<Record<TriggerType, (keyof PopperEvents)[]>> = {
// click类型,绑定onClick事件
click: ['onClick'],
// hover类型,绑定onMouseenter onMouseleave 事件
hover: ['onMouseenter', 'onMouseleave'],
// focus类型,绑定onFocus onBlur事件
focus: ['onFocus', 'onBlur'],
}
const mapEvents = (t: TriggerType) => {
triggerEventsMap[t].forEach(event => {
// 各个事件的响应函数都一样,popperEventsHandler函数内部针对event type进行处理
events[event] = popperEventsHandler
})
}
if (isArray(props.trigger)) {
Object.values(props.trigger).forEach(mapEvents)
} else {
mapEvents(props.trigger as TriggerType)
}
}
// popperOptions变化时,使用popperjs的setOptions方法重新设置,并update
watch(popperOptions, val => {
if (!popperInstance) return
popperInstance.setOptions(val)
popperInstance.update()
})
return {
doDestroy,
show,
hide,
onPopperMouseEnter,
onPopperMouseLeave,
onAfterEnter: () => {
emit('after-enter')
},
onAfterLeave: () => {
detachPopper()
emit('after-leave')
},
onBeforeEnter: () => {
emit('before-enter')
},
onBeforeLeave: () => {
emit('before-leave')
},
initializePopper,
isManualMode,
arrowRef,
events,
popperId,
popperInstance,
popperRef,
popperStyle,
triggerRef,
visibility,
}
}
usePopper总结:
usePopper方法中,定义了属性和方法,维护弹框的显示状态;usePopper方法中,调用第三方库popperjs控制弹框的实际显示位置;usePopper方法中,为trigger绑定相应的事件处理函数,控制弹框的展示隐藏。