Element-UI阅读理解(4) - 弹窗管理工具类PopupManager

4,929 阅读5分钟

前言

不推荐通篇阅读,建议将element-ui源码下载并运行到本地,按照自己的方式阅读源码,遇上不明白点可以来这里Ctrl+F搜索一下,看这里有没有记录。

这算不上是分享,只是自己对源码阅读理解的记录,请勿纠结行文杂乱(还是菜菜,写不好)

文章分两部部分:

  • PopupManager的简单介绍:以el-dialog 为例,来说明弹窗组件与工具类的基本原理;

  • 具体分析el-dialog的开关流程:包含对各个文件中的大部分的函数、生命周期函数、工具函数/类的具体分析,作为记录;(不推荐通篇阅读,建议阅读源码时遇到不懂的点,可以来查阅一下)

popupManager的简单介绍:

  • PopupManager以mixins的方式注入组件,拥有实例选项,props,data,watch,生命周期等等;
  • PopupManager是所有弹窗租件的公共代码;
  • PopupManage的主体有两个js文件构成:
    • index.js拥有实例选项,监听visible并处理,在生命周期函数内有弹窗管理逻辑执行;
    • popup-manage.js index.js的专属工具函数,管理弹窗;

以el-dialog 为例,来理解弹窗组件与工具类

需要注意的是: el-dialog并没有使用PopupMangage的全部内容,这里仅仅是以el-dialog为切入点来理解PopupManage

el-dialog只有component.vue一个文件,它监听visible并处理组件内的事件,定义组件挂载dom操作和注销生命周期,派发一些事件(与弹窗的管理无关);

index.js在beforeMount,beforeDestory两个生命周期内调用PopupManager的弹窗组管理相关的函数PopupManager中的register deregister closeModal函数;

index.js监听visible,执行以下两个流程:

  • 打开弹窗:open --> doOpen --> PopupManager.openModal
  • 关闭弹窗:close --> doClose --> PopupManager.closeModal

函数close open 中分别定义_closeTimer _openTimer两个计时器,参数是延时配置项closeDelay,作用是延时关闭组件,openDelay同理;open,close都方法各维护了,定时器的创建、清除,并执行真实开关组件的函数 doOpen,doCloseopenModal closeModal维护弹窗组件的管理栈modalStack,以及处理组件的遮罩层dom (dom也在popup-manager.js中创建)<body>的添加和移除;

具体分析流程,开关弹窗(枯燥)

popup-mangeer.js过一遍

//  element\src\utils\popup\popup-manager.js

变量let hasModal = false;Message-box组件的默认属性modalFade = true有关,用于生成div.v-model节点挂载<body>下作为遮罩层;

变量let hasInitZIndex = false; 所有的弹窗组件的z-index,都依赖popup-manager,hasInitZIndex 就只是一个等于false的变量,可有可无;

const getModal = function() {...} 创建div.v-model,并在上面注册事件,和组件将遮罩层设置挂载到body元素有关,el-dialog有这个配置项modal-append-to-body

let zIndex; 维护所有弹窗组件的z-index的值,新增的弹窗组件的z-index在变量zIndex的基础上+1,就是函数nextZIndex实现的功能;

const PopupMange = {..} 中的函数名即是函数的功能;

index.js

//  element\src\utils\popup\index.js

PopupManagermixins的方式注入组件,嵌入了当前组件,PopupManager用于管理弹窗组件,是与vue实例关联紧密的工具,拥有vue实例的所有选项; popup-manager.js是所有弹窗组件的公共区域;

let idSeed = 1;
let scrollBarWidth;
//  这个模块的全局变量
//  生命周期:beforeMount
beforeMount() { 
    this._popupId = 'popup-' + idSeed++;  
    PopupManager.register(this._popupId, this);
},
//  

生成id弹窗组件的标识,和组件实例一起保存在模块popup-manager.js的全局对象 const instances = {}中:

//  存储形式
register: function(id, instance) if (id && instance) { 
        instances[id] = instance;  
    }
}

打开el-dialog的流程

visibletrue,组件监听变化并处理

//  components.vue 中的操作
watch: { 
    visible(val) {  
    if (val) {  
        this.closed = false;    //  是否关闭的标志位
        this.$emit('open');     //  派发事件,组件上可以监听到
        this.$el.addEventListener('scroll',    //  与下拉组件相关 this.updatePopper); 
        this.$nextTick(() => {      //  设置滚动调的位置,this.$next() 是必要的
            this.$refs.dialog.scrollTop = 0;  
        });  
        if (this.appendToBody) {        //  是否要挂载到body元素上
            document.body.appendChild(this.$el); 
        }  
    } else {    
        this.$el.removeEventListener('scroll', this.updatePopper);      
            if (!this.closed) this.$emit('close'); 
            if (this.destroyOnClose) {      //  关闭时销毁 Dialog 中的元素
                this.$nextTick(() => {   
                    this.key++;   
                });    ]
            }
        } 
    }
}

visible与closed visiblePopupmixns的型式添加的props属性,控制组件的显示与否;closed是组件是否关闭的标志位,在data中;

rendered renderedPopupmixns的型式添加的data属性,控制组件默认slot的加载;

//  element\src\utils\popup\index.js
watch: {  
    visible(val) { 
        if (val) {   
            if (this._opening) return;      //  是否正在打开的标志位
            if (!this.rendered) {       //  data中的数据默认false       
                this.rendered = true;       
                Vue.nextTick(() => {  
                    this.open();        //  执行函数      
                });    
            } else {  
                this.open(); 
            } 
        } else {   
            this.close();   
        } 
    }
}
//  element\src\utils\popup\index.js 
open(options) {     //  目前还不知到那个组件会传options,el-dialog是没传的 
    if (!this.rendered) { 
        this.rendered = true;  
    }  
    const props = merge({}, this.$props || this, options);      //  合并对象
    if (this._closeTimer) {     //  定义在close()中一个定时器,延时关闭弹窗
        clearTimeout(this._closeTimer);
        this._closeTimer = null; 
    }  
    clearTimeout(this._openTimer); 
    const openDelay = Number(props.openDelay);
    if (openDelay > 0) {  
        this._openTimer = setTimeout(() => {        //定义定时器,延时打开
            this._openTimer = null;  
            this.doOpen(props);     //  执行doOpen
        }, openDelay); 
    } else {  
        this.doOpen(props); 
    }
}
//  element\src\utils\popup\index.js 
//  doOpen(props)
doOpen(props) {
···
willOpen    //  不知道那个弹窗组件有这个属性,el-dialog没有
···
if (this.opened) return;    //  是否打开弹窗的data属性
this._opening = true;   // 正在打开,标志位
const dom = this.$el;
const modal = props.modal;      //  是否需要遮罩层
const zIndex = props.zIndex;    //  似乎没有哪个组件有zIndex配置项
if (zIndex) {  PopupManager.zIndex = zIndex;}
//  获取属性值
//  if (mdal) {}  中:
PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), 
this.modalAppendToBody ? undefined : dom, props.modalClass, 
props.modalFade);
if (props.lockScroll) {}  //  与滚动条有关,配置项lockScoll
//  lock-scroll是否在 Dialog 出现时将 body 滚动锁定boolean—true
//  滚动条不是这次阅读的重点,getScrollBarWidth 一个几十行的工具函数,暂时略过

el-dialog文档:modal-append-to-body遮罩层是否插入至 body 元素上,若为 false,则遮罩层会插入至 Dialog的父元素上boolean类型,默认为true

this._closeTimer this._openTimer 定义 两个计时器;closeDelay是Popup的props,只在组件Popover中存在,作用是延时关闭组件,openDelay同理;

open,close都方法各维护了,定时器的创建、清除,并执行真实开关组件的函数 doOpen,doCloseopenModal closeModal真正对弹窗组件的管理(modalStack))以及将组件dombody的添加和移除

//  element\src\utils\popup\popup-manager.js
openModal: function(id, zIndex, dom, modalClass, modalFade) {
···
this.modalFade = modalFade;     //  不知道那个组件有这个属性
const modalStack = this.modalStack;   
for (let i = 0, j = modalStack.length; i < j; i++) { 
    const item = modalStack[i]; 
    if (item.id === id) { 
        return; 
    }
}
  //  初次运行openModal,this.modalStack目前是空数组
const modalDom = getModal();        //  创建div元素,并设置监听函数,将变量hasModal置为false
addClass(modalDom, 'v-modal');      //  用工具函数,给元素加类名并添加事件监听,touchmove click
modalFade       //  是popupManager的props属性默认为true
//  类名的添加和移除影响样式,实现动画
if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) {  
    dom.parentNode.appendChild(modalDom);
} elsedocument.body.appendChild(modalDom);
}
//  dom 是组件实例本身
//  dom.parentNode.nodeType !== 11 判断元素是否是DocumentFragment
if (zIndex) {  
    modalDom.style.zIndex = zIndex;

}modalDom.tabIndex = 0;
modalDom.style.display = '';
//  给遮罩层设置样式
this.modalStack.push({ id: id, zIndex: zIndex, modalClass: modalClass });
}
//  将弹窗实例的部分信息保存在 PopupManage.modalStack

dom.parentNode.nodeType !== 11参考文档

以上便是第一次打开弹窗的流程


关闭el-dialog的流程

visible置为false,组件监听变化并处理

关闭有两种方式:

  • 点击关闭按钮
  • 点击遮罩层(可配置是否要启用这个功能)

两者的流程基本一致,只是“点击遮罩层”需要经过配置项closeOnClickModal

//  element\packages\dialog\src\component.vue
handleClose() {  
    if (typeof this.beforeClose === 'function') {   
        this.beforeClose(this.hide);    //  
    } else {   
        this.hide();  
    }
},
hide(cancel) { 
    if (cancel !== false) {    
        this.$emit('update:visible', false);   
        this.$emit('close');  
        this.closed = true; 
    }
},

this.closeOnClickModal 来自props,组件的配置项,

文档:close-on-click-modal,是否可以通过点击 modal(遮罩层) 关闭 Dialog,默认true

el-dialog为例:

<el-dialog  
    :before-close="handleClose"
    @open="handleOpen">
</el-dialog>
    ···
    handleClose(done) { 
        this.$confirm('确认关闭?')
            .then(_ => { done(); }) 
            .catch(_ => {}); 
    }, 
    handleOpen() { 
        console.log("handleOpen, 监听open事件"); 
   }
//  this.beforeClose(this.hide); 

done 就是this.hide函数,是组件内置的函数,用与关闭dialog,并向外派发了两个事件:

this.$emit('update:visible', false);
this.$emit('close');

this.$emit('update:visible', false); 与.sync修饰符有关:文档

//  components.vue wacth visible为false 的逻辑:
this.$el.removeEventListener('scroll', this.updatePopper);      //  与滚动条相关
if (!this.closed) this.$emit('close');      //   派发事件,组件上可以监听
if (this.destroyOnClose) {  this.$nextTick(() => {   
    this.key++;     //  destroyOnClose 配置项,关闭时是否销毁 Dialog 中的元素, 销毁元素需要修改组件的key
});
//  index.js acth visible为false 的逻辑:
this.close();
//  index.js
close() {
    if (this.willClose && !this.willClose()) return//  没找到willClose
    if (this._openTimer !== null) {   
        clearTimeout(this._openTimer); 
        this._openTimer = null;
    } 
    clearTimeout(this._closeTimer);
    const closeDelay = Number(this.closeDelay); 
    if (closeDelay > 0) {  
        this._closeTimer = setTimeout(() => {    
            this._closeTimer = null;  
            this.doClose();
        }, closeDelay); 
    } else {    
        this.doClose(); 
    }
},
doClose() { 
    this._closing = truethis.onClose && this.onClose(); 
    if (this.lockScroll) {  
        setTimeout(this.restoreBodyStyle, 200);     //  看名字,应该是个与body元素的样式有关,当el-dialog的append-to-body为true时显示是隐藏了body的滚动条,有些细枝末节不必深究,看看名字就能了解;
    }
    this.opened = falsethis.doAfterClose();
},
doAfterClose() {  
    PopupManager.closeModal(this._popupId);  
    this._closing = false;
},

this._closeTimer this._openTimer 定义 两个计时器;closeDelay是Popup的props,只在组件Popover中存在,作用是延时关闭组件,openDelay同理;

open,close都方法各维护了,定时器的创建、清除,并执行真实开关组件的函数 doOpen,doCloseopenModal closeModal真正对弹窗组件的管理(modalStack))以及将组件dombody的添加和移除

if (!Vue.prototype.$isServer) {  
// handle `esc` key when the popup is shown  
    window.addEventListener('keydown', function(event) 
    ···
    ···
    //   监听键盘的 ‘Esc’ 关闭栈顶的弹窗
}

关闭流程和打开流程基本一样,没什么好讲的


上一个是:全局命令式组件Message


扩展阅读:

.sync修饰符 文档

element-ui官方组件:vue-popup