dialog组件分析
示例代码
<template>
<!-- 示例代码 -->
<div>
<el-button type="text" @click="outerVisible = true">点击打开外层 Dialog</el-button>
<el-dialog title="外层 Dialog" :visible.sync="outerVisible">
<el-dialog
width="30%"
title="内层 Dialog"
:visible.sync="innerVisible"
append-to-body>
</el-dialog>
<div slot="footer" class="dialog-footer">
<el-button @click="outerVisible = false">取 消</el-button>
<el-button type="primary" @click="innerVisible = true">打开内层 Dialog</el-button>
</div>
</el-dialog>
</div>
</template>
- 看下两层对话框是怎么处理的
- 首先来看下对话框初始化显示的处理 逻辑
dialog组件代码
<template>
<transition
name="dialog-fade"
@after-enter="afterEnter"
@after-leave="afterLeave">
<div
v-show="visible"
class="el-dialog__wrapper"
@click.self="handleWrapperClick">
<div
role="dialog"
:key="key"
aria-modal="true"
:aria-label="title || 'dialog'"
:class="['el-dialog', { 'is-fullscreen': fullscreen, 'el-dialog--center': center }, customClass]"
ref="dialog"
:style="style">
<div class="el-dialog__header">
<slot name="title">
<span class="el-dialog__title">{{ title }}</span>
</slot>
<button
type="button"
class="el-dialog__headerbtn"
aria-label="Close"
v-if="showClose"
@click="handleClose">
<i class="el-dialog__close el-icon el-icon-close"></i>
</button>
</div>
<div class="el-dialog__body" v-if="rendered"><slot></slot></div>
<div class="el-dialog__footer" v-if="$slots.footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</transition>
</template>
<script>
import Popup from '@/utils/popup';
import Migrating from '@/mixins/migrating';
import emitter from '@/mixins/emitter';
export default {
name: 'ElDialog',
mixins: [Popup, emitter, Migrating],
props: {
title: {
type: String,
default: '',
},
modal: {
type: Boolean,
default: true,
},
modalAppendToBody: {
type: Boolean,
default: true,
},
appendToBody: {
type: Boolean,
default: false,
},
lockScroll: {
type: Boolean,
default: true,
},
closeOnClickModal: {
type: Boolean,
default: true,
},
closeOnPressEscape: {
type: Boolean,
default: true,
},
showClose: {
type: Boolean,
default: true,
},
width: String,
fullscreen: Boolean,
customClass: {
type: String,
default: '',
},
top: {
type: String,
default: '15vh',
},
beforeClose: Function,
center: {
type: Boolean,
default: false,
},
destroyOnClose: Boolean,
},
data() {
return {
closed: false,
key: 0,
};
},
watch: {
visible(val) {
if (val) {
this.closed = false;
this.$emit('open');
this.$el.addEventListener('scroll', this.updatePopper);
this.$nextTick(() => {
this.$refs.dialog.scrollTop = 0;
});
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
} else {
this.$el.removeEventListener('scroll', this.updatePopper);
if (!this.closed) this.$emit('close');
if (this.destroyOnClose) {
this.$nextTick(() => {
this.key++;
});
}
}
},
},
computed: {
style() {
const style = {};
if (!this.fullscreen) {
style.marginTop = this.top;
if (this.width) {
style.width = this.width;
}
}
return style;
},
},
methods: {
getMigratingConfig() {
return {
props: {
size: 'size is removed.',
},
};
},
handleWrapperClick() {
if (!this.closeOnClickModal) return;
this.handleClose();
},
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;
}
},
updatePopper() {
this.broadcast('ElSelectDropdown', 'updatePopper');
this.broadcast('ElDropdownMenu', 'updatePopper');
},
afterEnter() {
this.$emit('opened');
},
afterLeave() {
this.$emit('closed');
},
},
mounted() {
if (this.visible) {
this.rendered = true;
this.open();
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
}
},
destroyed() {
// if appendToBody is true, remove DOM node after destroy
if (this.appendToBody && this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el);
}
},
};
</script>
- 我们来看下这个初始化吧
// 可以看到一开始我们是隐藏的那这个就不用看了
mounted() {
if (this.visible) {
this.rendered = true;
this.open();
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
}
},
- 在来看下这边有引入一个
mixins
Popup 看下这个初始化有没做什么
// 初始化了 这个_popupId 然后往PopupManager 注册了一个东西
beforeMount() {
this._popupId = `popup-${idSeed++}`;
PopupManager.register(this._popupId, this);
},
// 来看下 PopupManager.register
// instances是一个内部变量存储对象 把这个dialog组件 存起来
register(id, instance) {
if (id && instance) {
instances[id] = instance;
}
},
- OK 那我们继续
- 那么首先要显示第一步肯定就是 visible=true
- 我们来看下发生肾么事了 本身组件这边有个监听
watch: {
visible(val) {
if (val) {
// 那么肯定就是走这边逻辑了
this.closed = false;
// OK 这边发射这个 open 事件
this.$emit('open');
// 这个监听浏览器的滚动 然后就处罚这个函数
// updatePopper() {
// this.broadcast('ElSelectDropdown', 'updatePopper');
// this.broadcast('ElDropdownMenu', 'updatePopper');
// },
// 我们可以看到 broadcast 向下级组件广播 让他们更新 Popper
// 这个主要是为了 让dialog里面有包含 select 等组件 是打开状态的时候需要更新位置
// 这个下面分析到 select 再说
this.$el.addEventListener('scroll', this.updatePopper);
// 然后dialog 滚动到顶部
this.$nextTick(() => {
this.$refs.dialog.scrollTop = 0;
});
// 这个默认是false 那就显示在原来的位置咯
// 那么到这里 dialog 就显出出来了
// 但是 这边什么 遮罩层啥的呢 是怎么出来的
// OK 我们别忘了 这边mixins的 Popup 我们来看下做了什么
if (this.appendToBody) {
document.body.appendChild(this.$el);
}
} else {
this.$el.removeEventListener('scroll', this.updatePopper);
if (!this.closed) this.$emit('close');
if (this.destroyOnClose) {
this.$nextTick(() => {
this.key++;
});
}
}
},
},
Popup
我们看这边也监听了 visible
watch: {
visible(val) {
// OK 这边是显示的 那么我进来
if (val) {
// 那么第一次 这个是false 继续
if (this._opening) return;
// 第一次也是false 继续进去
if (!this.rendered) {
this.rendered = true;
// 然后等下次事件环 调用this.open 我们来看下
Vue.nextTick(() => {
this.open();
});
} else {
this.open();
}
} else {
this.close();
}
},
},
- this.open
open(options) {
if (!this.rendered) {
this.rendered = true;
}
// 那么 来到这边 合并下options 这里没啥
const props = merge({}, this.$props || this, options);
// 这里没有 我们跳过
if (this._closeTimer) {
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);
}, openDelay);
} else {
// 到这里 我们来看
this.doOpen(props);
}
},
- this.doOpen(props)
doOpen(props) {
// 是否是服务端渲染
if (this.$isServer) return;
// 那这边也没有管我们的事 继续
if (this.willOpen && !this.willOpen()) return;
if (this.opened) return;
// OK 这伙 标识符设置上 _opening = true
this._opening = true;
// 这个 this.$el 就是dialog的最外层元素了
const dom = this.$el;
// 这个就是 遮罩层了 dialog默认是 true
const { modal } = props;
// 看看有没有传 zIndex 基本是没有传了
const { zIndex } = props;
if (zIndex) {
PopupManager.zIndex = zIndex;
}
// 有遮罩层
if (modal) {
// 如果正在关闭 现在基本跟我们没有 关系
if (this._closing) {
PopupManager.closeModal(this._popupId);
this._closing = false;
}
// OK 这边我们 看下 PopupManager这个类 打开openModal
// 传入的有这个 dialog的 this._popupId
// 下个层级 PopupManager.nextZIndex()
// nextZIndex() {
// return PopupManager.zIndex++;
// },
// 第三个 默认dialog是 true
// 第四个第五个 是传的 modal弹层的显示时候的 class 然后是否是淡入淡出
// 继续看 openModal
PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), this.modalAppendToBody ? undefined : dom, props.modalClass, props.modalFade);
// 现在模态框是现实出来了
// 是否让下面的是锁定的 默认是的
if (props.lockScroll) {
// 如果是的话
// 这边的话 判断body是不是有 el-popup-parent--hidden
this.withoutHiddenClass = !hasClass(document.body, 'el-popup-parent--hidden');
// 没有的话
if (this.withoutHiddenClass) {
// 获取到 body的 padding-right
this.bodyPaddingRight = document.body.style.paddingRight;
this.computedBodyPaddingRight = parseInt(getStyle(document.body, 'paddingRight'), 10);
}
// 获取到 滚动条的 宽度
scrollBarWidth = getScrollBarWidth();
// 当前body是否是 超出的状态 也就是说有没有滚动条了
const bodyHasOverflow = document.documentElement.clientHeight < document.body.scrollHeight;
// 查看body overflowY 属性
const bodyOverflowY = getStyle(document.body, 'overflowY');
// 总的来说这边条件就是说 body边上 有滚动条了 那么就给body加上 相应的 padding-right
// 免得 body 设置上 overflow 为 hidden的时候滚动条消失 页面变宽 发生页面的抖动
if (scrollBarWidth > 0 && (bodyHasOverflow || bodyOverflowY === 'scroll') && this.withoutHiddenClass) {
document.body.style.paddingRight = `${this.computedBodyPaddingRight + scrollBarWidth}px`;
}
addClass(document.body, 'el-popup-parent--hidden');
}
}
// 如果dialog外层是没有定位的话 那么就加上 absolute
if (getComputedStyle(dom).position === 'static') {
dom.style.position = 'absolute';
}
// 然后给 dialog 加上层级 因为刚刚给弹出层加上了层级 所以这边比刚刚高上一个层级
dom.style.zIndex = PopupManager.nextZIndex();
this.opened = true;
// 所以就是打开来了 有监听onOpen的执行
this.onOpen && this.onOpen();
this.doAfterOpen();
// 那么初始化打开就完全了哦
// 接下来在看下关闭
},
// doAfterOpen
doAfterOpen() {
// 设置这个标识为 false
this._opening = false;
},
- PopupManager.openModal
openModal(id, zIndex, dom, modalClass, modalFade) {
if (Vue.prototype.$isServer) return;
if (!id || zIndex === undefined) return;
this.modalFade = modalFade;
// 这个栈 默认是个空的数组[] 当前的this指得是PopupManager这个类了
const { modalStack } = this;
// 找下看看有没有这个modal 第一次 当然是没有咯
for (let i = 0, j = modalStack.length; i < j; i++) {
const item = modalStack[i];
if (item.id === id) {
return;
}
}
// 然后这边就是 getModal() 我们在来看
const modalDom = getModal();
// 然后给这个模态框加这个样式 然后是淡入淡出那就在加个样式
addClass(modalDom, 'v-modal');
if (this.modalFade && !hasModal) {
addClass(modalDom, 'v-modal-enter');
}
// 有自己传的样式加上
if (modalClass) {
const classArr = modalClass.trim().split(/\s+/);
classArr.forEach((item) => addClass(modalDom, item));
}
// 200毫秒后去掉 v-modal-enter
setTimeout(() => {
removeClass(modalDom, 'v-modal-enter');
}, 200);
// 添加上 第一次没有家在body上面
if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) {
dom.parentNode.appendChild(modalDom);
} else {
document.body.appendChild(modalDom);
}
// 添加上层级
if (zIndex) {
modalDom.style.zIndex = zIndex;
}
modalDom.tabIndex = 0;
modalDom.style.display = '';
// 这个模态框的 栈 加入当前的模态框 属性有 id 层级 class
// 然后我们在回头看 doOpen
this.modalStack.push({ id, zIndex, modalClass });
},
- getModal
const getModal = function () {
if (Vue.prototype.$isServer) return;
// 解析出 modalDom 那么目前肯定是 undefined咯
let { modalDom } = PopupManager;
if (modalDom) {
hasModal = true;
} else {
// 来到这里
hasModal = false;
// 创建一个div 给这个赋值上
modalDom = document.createElement('div');
PopupManager.modalDom = modalDom;
// 模态框屏蔽这个 touchmove
modalDom.addEventListener('touchmove', (event) => {
event.preventDefault();
event.stopPropagation();
});
// 模态框监听 click 时间 这个一般就是作用就是点击模态框的时候关闭 对话框
modalDom.addEventListener('click', () => {
PopupManager.doOnModalClick && PopupManager.doOnModalClick();
});
}
// 返回这个 modalDom
// 接下来往回去看 openModal
return modalDom;
};
朋友们按照我们的示例 第二次打开里面一层的对话框 那么当前就是有两个了
- 我们来看下流程 有什么不同
- 对话框组件那边是 没啥不同了
- 我们来看下 Popup 这边的 modal框逻辑来看下 因为两个对话框的话 饿了么 组件设计其实是只有一个 modal 遮罩层的
- 我们来看下处理
- 进入到 PopupManager.openModal(...)
openModal(id, zIndex, dom, modalClass, modalFade) {
if (Vue.prototype.$isServer) return;
if (!id || zIndex === undefined) return;
this.modalFade = modalFade;
const { modalStack } = this;
for (let i = 0, j = modalStack.length; i < j; i++) {
const item = modalStack[i];
if (item.id === id) {
return;
}
}
// 来看这里
const modalDom = getModal();
// 然后对刚刚的 遮罩层 加这个 不过已经有了
addClass(modalDom, 'v-modal');
// 然后加个动画 这个就是刚刚为啥 200毫秒要删掉了
if (this.modalFade && !hasModal) {
addClass(modalDom, 'v-modal-enter');
}
//
if (modalClass) {
const classArr = modalClass.trim().split(/\s+/);
classArr.forEach((item) => addClass(modalDom, item));
}
setTimeout(() => {
removeClass(modalDom, 'v-modal-enter');
}, 200);
if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) {
dom.parentNode.appendChild(modalDom);
} else {
document.body.appendChild(modalDom);
}
// 改变下层级
if (zIndex) {
modalDom.style.zIndex = zIndex;
}
modalDom.tabIndex = 0;
modalDom.style.display = '';
// 继续把当前这个模态框的状态加入到这个栈中
this.modalStack.push({ id, zIndex, modalClass });
// 然后就是 继续 看doOpen 跟刚刚的一样了
},
const getModal = function () {
if (Vue.prototype.$isServer) return;
let { modalDom } = PopupManager;
// 因为我们刚刚已经赋值了 所以已经有了 所以直接返回了 刚刚那个遮罩层
if (modalDom) {
hasModal = true;
} else {
// ...
}
return modalDom;
};
感觉今天这个写的有所进步啊
今天先到这边 明天看下 把close 关闭的流程梳理下 。。。
更新下
继续搞起来
关闭那么 visible = false
// dialog component 这边的处理
visible(val) {
if (val) {
// ...
} else {
// 移除这个事件 因为现实的时候有 监听
this.$el.removeEventListener('scroll', this.updatePopper);
if (!this.closed) this.$emit('close');
// prop 一个属性 关闭时是否销毁Dialog中的元素
if (this.destroyOnClose) {
// 因为模板中有个 <div role="dialog" :key="key"></div> 所以改变key值 就会让vue销毁里面的元素
// OK 那么这边及解读完了 接下来来看下 mixins中的 Popup
this.$nextTick(() => {
this.key++;
});
}
}
},
- Popup 下面的watch
visible(val) {
if (val) {
// ...
} else {
// Ok 直接走这边来
this.close();
}
},
close() {
// OK 这些没有继续
if (this.willClose && !this.willClose()) return;
// 也没有
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
doClose() {
// 设置这个标识符 正在关闭
this._closing = true;
// 这边也没有
this.onClose && this.onClose();
// 是否是锁定的 是的话就恢复body原来的样式
// restoreBodyStyle() {
// if (this.modal && this.withoutHiddenClass) {
// document.body.style.paddingRight = this.bodyPaddingRight;
// removeClass(document.body, 'el-popup-parent--hidden');
// }
// this.withoutHiddenClass = true;
// },
if (this.lockScroll) {
setTimeout(this.restoreBodyStyle, 200);
}
// 标识符设置
this.opened = false;
// 在来看这个方法
this.doAfterClose();
},
- popup -> doAfterClose
doAfterClose() {
// 调用这个 这个要小小的看下 传入当前的ID
PopupManager.closeModal(this._popupId);
this._closing = false;
},
closeModal(id) {
// 取出这个栈 因为刚刚我们是打开了 两个不同的 dialog 所以现在这个栈里面有两个对象
// 我们假设是 [{id: 1, zIndex: 2000, modalClass: {}}, {id: 2, zIndex: 2002, modalClass: {}}]
const { modalStack } = this;
const modalDom = getModal();
// OK 我们有两个 那么大于0 进去
if (modalStack.length > 0) {
// 取出最后一个
const topItem = modalStack[modalStack.length - 1];
// 最后一个 跟当前要关闭的 modal 是一个
if (topItem.id === id) {
// 如果有当前的这个有modalClass 那么把这些个class都去掉
if (topItem.modalClass) {
const classArr = topItem.modalClass.trim().split(/\s+/);
classArr.forEach((item) => removeClass(modalDom, item));
}
// 最后一个删除掉
modalStack.pop();
// 删除掉还有一个的话
if (modalStack.length > 0) {
// 那么我们就设置这个的 层级为 现在的最后一个
// 也就是id是2的没了 那么就设置成id为1这个对象的层级
// 一般来说就是 流程是
// modal 层级 2001 对话框层级 2002
// 在打开一个对话框 modal层级2003 对话框层级2004
// 关闭一个对话框 modal 层级变为又要变成2001放在 层级在第一个对话框的下面
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;
}
}
}
}
// 这边
if (modalStack.length === 0) {
// 我感觉如果是 没有栈了 那就是没有modal啊 这边感觉也没啥意义吧
if (this.modalFade) {
addClass(modalDom, 'v-modal-leave');
}
setTimeout(() => {
if (modalStack.length === 0) {
// 元素给删掉 然后复原下咯 那就差不多了啊 基本也没了
if (modalDom.parentNode) modalDom.parentNode.removeChild(modalDom);
modalDom.style.display = 'none';
PopupManager.modalDom = undefined;
}
removeClass(modalDom, 'v-modal-leave');
}, 200);
}
},
这边也基本是分析完了
大家一起学习探讨