前言
不推荐通篇阅读,建议将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,doClose
;openModal 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
PopupManager
以mixins
的方式注入组件,嵌入了当前组件,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的流程
visible
为true
,组件监听变化并处理
// 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
visible
是Popup
以mixns
的型式添加的props
属性,控制组件的显示与否;closed
是组件是否关闭的标志位,在data
中;
rendered
rendered
是Popup
以mixns
的型式添加的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,doClose
;openModal closeModal
真正对弹窗组件的管理(modalStack
))以及将组件dom
在body
的添加和移除
// 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);
} else {
document.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 = true;
this.onClose && this.onClose();
if (this.lockScroll) {
setTimeout(this.restoreBodyStyle, 200); // 看名字,应该是个与body元素的样式有关,当el-dialog的append-to-body为true时显示是隐藏了body的滚动条,有些细枝末节不必深究,看看名字就能了解;
}
this.opened = false;
this.doAfterClose();
},
doAfterClose() {
PopupManager.closeModal(this._popupId);
this._closing = false;
},
this._closeTimer this._openTimer
定义 两个计时器;closeDelay
是Popup的props,只在组件Popover中存在,作用是延时关闭组件,openDelay同理;
open,close
都方法各维护了,定时器的创建、清除,并执行真实开关组件的函数 doOpen,doClose
;openModal closeModal
真正对弹窗组件的管理(modalStack
))以及将组件dom
在body
的添加和移除
if (!Vue.prototype.$isServer) {
// handle `esc` key when the popup is shown
window.addEventListener('keydown', function(event)
···
···
// 监听键盘的 ‘Esc’ 关闭栈顶的弹窗
}
关闭流程和打开流程基本一样,没什么好讲的
上一个是:全局命令式组件Message