Popup
是一个层级关系(z-index
)管理工具类,主要用于管理组件的层级关系,例如message-box
,dialog
组件。在element-ui源码中,主要分为两个popup-manger.js
和popup mixin
这两部分,我们先看下popup mixin
这个文件。
Popup
此阶段不需要深究PopupManager的内部原理,直接调用PopupManager对象的方法即可。为了方便沟通,将引入了popup-mixin的单文件vue组件命名。定义为为弹窗组件,弹窗的灰色蒙层命名为modalDompopup.js
是一个mixin混入,功能清单如下:
- 引入
popupManger
-
beforeMount
周期时,调用PopupManager
对象的注册方法 -
beforeDestroy
周期中,调用PopupManager
对象的注销方法 -
openModa
方法,设置弹窗组件的z-index
,调用PopupManager.openModal
方法 -
closeModal
方法,调用PopupManager.closeModal
方法
为了方便大家阅读,下面列出的代码相对源码来说有所删减,只保留了核心功能的代码
import merge from '../merge'
import PopupManager from './popup-manager'
let idSeed = 1
export default {
props: {
visible: {
type: Boolean,
default: false
},
modalAppendToBody: {
type: Boolean,
default: false
},
closeOnPressEscape: {
type: Boolean,
default: false
},
closeOnClickModal: {
type: Boolean,
default: false
}
},
beforeMount() {
this._popupId = 'popup-' + idSeed++
PopupManager.register(this._popupId, this)
},
beforeDestroy() {
PopupManager.register(this._popupId)
PopupManager.closeModal(this._popupId)
},
data() {
return {
_popupId: null
}
},
watch: {
// 对于watch选项混入时,mixin内的和弹窗组件内的代码都会执行
visible(val) {
if (val) {
if (this._opening) return
// 判断是否是第一次渲染
if (!this.rendered) {
this.rendered = true
this.$nextTick(() => {
this.open()
})
} else {
this.open()
}
}
}
},
methods: {
open(options) {
if (!this.rendered) {
this.rendered = true
}
// 选项合并
const props = merge({}, options, this.$props)
this.doOpen(props)
},
doOpen(props) {
if(this.opended) return
this._opening = true
const dom = this.$el
PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), this.modalAppendToBody ? undefined : dom)
// 确保z-index属性能生效,z-index只能在position属性值为relative或absolute或fixed的元素上有效。
if (getComputedStyle(dom).position === 'static') {
dom.style.position = 'absolute'
}
// 设置组件的z-index,保证组件的层级在modalDom之上
dom.style.zIndex = PopupManager.nextZIndex()
this.opened = true
// 执行onopen回调函数
this.onOpen && this.onOpen()
this.doAfterOpen()
},
doAfterOpen() {
this._opening = false
},
close() {
this.doClose()
},
doClose() {
this._closing = true
this.onClose && this.onClose()
this.opened = false
this.doAfterClose()
},
doAfterClose() {
// 关闭modal
PopupManager.closeModal(this._popupId)
this._closing = false
}
}
}
export { PopupManager }
PopupManager
现在我们再看下popupManager的源码,同样为了方便阅读,对部分非核心代码做了删减,阅读时请配合此demo一起观看:[element-ui嵌套dialog] (https://jsfiddle.net/api/post...)
/* eslint-disable */
import Vue from 'vue'
import { addClass, removeClass } from 'element-ui/src/utils/dom'
let hasInitZIndex = false
let zIndex
/**
* 返回modalDom元素
* 第一次调用时,会创建一个div,并绑定click事件,并将此div赋值给PopupManager的modalDom属性
* 第二次调用时直接返回PopupManager.modalDom
*/
const getModal = function() {
if (Vue.prototype.$isServer) return
let modalDom = PopupManager.modalDom
if (!modalDom) {
modalDom = document.createElement('div')
PopupManager.modalDom = modalDom
modalDom.addEventListener('touchmove', function(event) {
event.preventDefault()
event.stopPropagation()
})
modalDom.addEventListener('click', function() {
PopupManager.doOnModalClick && PopupManager.doOnModalClick()
})
}
return modalDom
}
const instances = {}
const PopupManager = {
// 是否开启modal淡入淡出动画
modalFade: true,
// 注册弹窗组件
register: function(id, instance) {
if (id && instance) {
instances[id] = instance
}
},
// 注销弹窗组件
deregister: function(id) {
if (id) {
instances[id] = null
delete instances[id]
}
},
// 根据id获取弹窗组件
getInstance: function(id) {
return instances[id]
},
nextZIndex: function() {
return PopupManager.zIndex++
},
// 虚拟的modal蒙层数组,每一个弹窗组件对应一个虚拟的modal蒙层,实际上在页面中只存在一个modal蒙层,所有的弹窗组件共用一个蒙层即可,可以参考嵌套弹窗demo
modalStack: [],
/**
* 页面中添加modalDom,在上面的额popup-mixin的doOpen方法里面,就调用了PopupManager.openModal方法
* 弹窗组件调用过openModal方法后,PopupManager会将弹窗组件(id,zIndex)push进modalStack中进行管理
* 弹窗窗组件调用PopupManager.closeModal方法后,PopupManager会将弹窗组件从modalStack中删除
*/
openModal: function(id, zIndex, dom) {
const modalStack = this.modalStack
for (let i = 0, j = modalStack.length; i < j; i++) {
const item = modalStack[i]
if (item.id === id) {
return
}
}
if (!id || !zIndex) return
// 调用getModal方法,获取到真实的modalDom元素
var modalDom = getModal()
addClass(modalDom, 'v-modal')
// 根据传入的dom元素,判断modalDom应该插入的节点
if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) {
dom.parentNode.appendChild(modalDom)
} else {
document.body.appendChild(modalDom)
}
// 设置modalDom的zIndex,
// 回到popup-mixin代码的openModa处,可以发现传入的zInxex值是PopupManager.nextZIndex(),然后弹窗组件又给自己设置了zindex,dom.style.zIndex = PopupManager.nextZIndex()
//这样就能确保弹窗组件的zIndex比modalDom高,展示在modalDom之上
modalDom.style.zIndex = zIndex
modalDom.style.display = ''
this.modalStack.push({ id, zIndex })
},
// 当有多个弹窗组件时,只有最上面的弹窗组件层级关系是在modalDom上,所以此时点击modal,应该去执行最上层弹窗组件的回调方法
doOnModalClick: function() {
var topItem = this.modalStack[this.modalStack.length - 1]
if (topItem) {
const instance = PopupManager.getInstance(topItem.id)
if (instance && instance.closeOnClickModal) {
instance.close()
}
}
},
// 关闭弹窗,阅读之前,请先看一下关闭嵌套dialog时,各个弹窗与modalDom的表现
closeModal: function(id) {
const modalStack = this.modalStack
const modalDom = getModal()
var popup = this.getInstance(id)
//modalStack中删除对应的弹窗组件
if (modalStack.length > 0) {
const topItem = modalStack[modalStack.length - 1]
if (topItem.id === id) {
modalStack.pop()
if (modalStack.length > 0) {
/* 如果是最上层的弹窗组件,且下面还存在其他的弹窗组件,则重新设置modalDOm的zindex,modalStack中保存的zIndex并不是弹窗组件dom的Zindex,而是执行PopupManager.openModal方法时传入的zindex,弹窗组件dom的Zindex是在执行PopupManager.openModal方法后再设置的*/
modalDom.style.zIndex = modalStack[modalStack.length - 1].zIndex
}
} else {
// 异常情况处理
for (let i = modalStack.length - 1; i >= 0; i--) {
if (modalStack[i].id === id) {
modalStack.splice(i, 1)
break
}
}
}
}
// 如果没有其他弹窗组件了,则隐藏modalDom
if (this.modalStack.length === 0) {
if (modalDom.parentNode) modalDom.parentNode.removeChild(modalDom)
modalDom.style.display = 'none'
PopupManager.modalDom = undefined
}
}
}
Object.defineProperty(PopupManager, 'zIndex', {
configurable: true,
get() {
if (!hasInitZIndex) {
hasInitZIndex = true
zIndex = zIndex || 2000
}
return zIndex
},
set(value) {
zIndex = value
}
})
const getTopPopup = function() {
if (PopupManager.modalStack.length > 0) {
const topPopup = PopupManager.modalStack[PopupManager.modalStack.length - 1]
if (!topPopup) return
const instance = PopupManager.getInstance(topPopup.id)
return instance
}
}
if (!Vue.prototype.$isServer) {
// handle `esc` key when the popup is shown
window.addEventListener('keydown', function(event) {
if (event.keyCode === 27) {
const topPopup = getTopPopup()
if (topPopup && topPopup.closeOnPressEscape) {
topPopup.handleClose
? topPopup.handleClose()
: topPopup.handleAction
? topPopup.handleAction('cancel')
: topPopup.close()
}
}
})
}
export default PopupManager
后记
出于减少读者阅读量的原因,本文列出的代码只包括核心功能,有条件的同学可以直接阅读源码,看一下官方是如何实现lockOnScroll
,delayOpen
等相关功能的