element-ui 源码分析-工具篇:popup

Popup是一个层级关系(z-index)管理工具类,主要用于管理组件的层级关系,例如message-box,dialog组件。在element-ui源码中,主要分为两个popup-manger.jspopup mixin这两部分,我们先看下popup mixin这个文件。

Popup

此阶段不需要深究PopupManager的内部原理,直接调用PopupManager对象的方法即可。为了方便沟通,将引入了popup-mixin的单文件vue组件命名。定义为为弹窗组件,弹窗的灰色蒙层命名为modalDom
popup.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等相关功能的

相关推荐