废了废了,中间一堆事耽误了迟迟没有更新。来吧继续阅读组件源码,dialog组件安排上。个人觉得dialog组件的难点在于弹出框的流程,特别是层级的处理。 当然其他小的知识点也是不少的,这就是阅读源码的快乐。
源码大致分为三部分阅读,先易后难没毛病
打开packages/dialog/src/component.vue
@click.self="handleWrapperClick">:key="key"aria-modal="true":aria-label="title || 'dialog'":class="['el-dialog', { 'is-fullscreen': fullscreen, 'el-dialog--center': center }, customClass]"ref="dialog":style="style">{{ title }} 总结:1、亮点:通过改变key值来销毁div(铁子们还知道vue中key的其他用法吗?)2、vue内置transition组件及其钩子函数的灵活运用3、@click.self 指触发元素自身时生效
还是packages/dialog/src/component.vue
总结:1、了解了dialog组件的整体流程,以及dialog各个Attributes参数的实际作用2、通过dialog组件可以看到elementUI的严谨性,如事件的绑定与移除,元素的插入与销毁, n e x t T i c k 合理利用 3 、复习了 . s y n c 语法糖, nextTick合理利用3、复习了.sync语法糖, nextTick合理利用3、复习了.sync语法糖,emit(‘update:visible’)的用途
打开packages/src/mixins/emitter先来点开胃菜如上文所说,emitter.js定义了dispatch和broadcast方法用来派发和广播事件1)dispatch是用于派发事件到父组件以及更上级别的指定组件进行接收的2)broadcast方法主要用于将数据或者方法广播到子组件以及子孙指定组件进行接收
/**
* 广播方法定义
* @param String componentName 组件名称
* @param String eventName 事件名称
* @param Object params 参数
*/
function broadcast(componentName, eventName, params) {// 遍历子组件,对子组件的componentName进行匹配// 阅读源码,发现很多组件除了定义name属性外,还定义了componentName了,这里就了解componentName的作用this.$children.forEach(child => {var name = child.$options.componentName;if (name === componentName) {// 子组件中与传入的componentName相等时,则在子组件中执行eventName方法,参数为params// 注意$emit会触发组件中的$on事件(vue中内置的$emit、$on)// 通过apply将this指向为当前组件,apply第二个参数为一个数组child.$emit.apply(child, [eventName].concat(params));} else {// 如果不存在则继续执行broadcast方法,this指向子组件broadcast.apply(child, [componentName, eventName].concat([params]));}});
}
export default {methods: {/** * 派发方法定义 * @param String componentName 组件名称 * @param String eventName 事件名称 * @param Object params 参数 */dispatch(componentName, eventName, params) {// 通过while循环找到对应的父组件(找父组件的场景在平常开发中也会用到)// 定义父组件对象,如果该组件上面没有对象,则parent为根组件var parent = this.$parent || this.$root;var name = parent.$options.componentName;// 当父组件对象存在时且父组件名称不等于componentName时,则改变parent值,并将parent值向上赋值;当parent不存在或者name === componentName时,跳出循环while (parent && (!name || name !== componentName)) {parent = parent.$parent;// 如果父组件存在,取父组件的componentNameif (parent) {name = parent.$options.componentName;}}// 找到对应的父组件时,执行该组件中eventName方法,参数为paramsif (parent) {parent.$emit.apply(parent, [eventName].concat(params));}},broadcast(componentName, eventName, params) {broadcast.call(this, componentName, eventName, params);}}
};
打开packages/src/utils/popup/index.js重点来了,来吧一起了解下dialog弹框的流程**dialog弹框打开的流程总结:**1、通过mixins混入popup/index.js2、watch监听visible属性的变化,为true时先执行this.open方法,再执行this.doOpen方法3、在this.doOpen方法中调用PopupManager.openModal打开遮罩4、lockScroll属性为true时,给body设置overflow: hidden;实现弹框打开时将body滚动锁定5、给当前的dialog组件加上层级,层级的高度比遮罩高一层6、如果打开多个弹框,公用一个遮罩,通过openModal控制遮罩的层级(下面会分析PopupManager.openModal方法)
**dialog弹框关闭的流程总结:**1、watch监听visible属性的变化,为false时先执行this.close方法,再执行this.doClose方法2、在this.doAfterClose方法中调用PopupManager.closeModal关闭遮罩3、关闭遮罩时如果存在多个弹框,需将遮罩的层级降为上一个弹框的层级(下面会分析PopupManager.closeModal方法)
import Vue from 'vue';
import merge from 'element-ui/src/utils/merge';
import PopupManager from 'element-ui/src/utils/popup/popup-manager';
import getScrollBarWidth from '../scrollbar-width';
import { getStyle, addClass, removeClass, hasClass } from '../dom';
let idSeed = 1;
let scrollBarWidth;
export default {props: {visible: {type: Boolean,default: false},openDelay: {},closeDelay: {},zIndex: {},modal: {type: Boolean,default: false},modalFade: {type: Boolean,default: true},modalClass: {},modalAppendToBody: {type: Boolean,default: false},lockScroll: {type: Boolean,default: true},closeOnPressEscape: {type: Boolean,default: false},closeOnClickModal: {type: Boolean,default: false}},beforeMount() {// 生成一个_popupId,调用PopupManager.register将当前组件的实例对象注册到instances中this._popupId = 'popup-' + idSeed++;PopupManager.register(this._popupId, this);},// 关闭时 销毁对应的实例,并移除body的class类名beforeDestroy() {PopupManager.deregister(this._popupId);PopupManager.closeModal(this._popupId);this.restoreBodyStyle();},data() {return {opened: false,bodyPaddingRight: null,computedBodyPaddingRight: 0,withoutHiddenClass: true,rendered: false};},watch: {visible(val) {// 同dialog组件一样,也是监听visibleif (val) {if (this._opening) return;if (!this.rendered) {this.rendered = true;Vue.nextTick(() => {// 第一次进入到这里this.open();});} else {this.open();}} else {// 进入关闭的流程this.close();}}},methods: {open(options) {if (!this.rendered) {this.rendered = true;}// 通过merge方法 合并props,这里没用到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);}},doOpen(props) {// 是否是服务端渲染if (this.$isServer) return;if (this.willOpen && !this.willOpen()) return;if (this.opened) return;// 这里 _opening = truethis._opening = true;// 这个 this.$el 就是dialog的el-dialog__wrapper元素const dom = this.$el;// modal属性 是否需要遮罩层const modal = props.modal;const zIndex = props.zIndex;// 第一次props.zIndex为undefinedif (zIndex) {PopupManager.zIndex = zIndex;}// 有遮罩层if (modal) {// 如果正在关闭现在基本跟我们没有 关系if (this._closing) {PopupManager.closeModal(this._popupId);this._closing = false;}/** * PopupManager.openModal是用来控制灰色遮罩的打开 * @param String _popupId 弹窗的id * @param String PopupManager.nextZIndex 弹窗的zIndex层级 * @param Object this.modalAppendToBody ? undefined : dom 如果设置modal-append-to-body属性,传入undefined,否则传入当前组件 * @param String modalClass modal弹层的显示时候的 class * @param Boolean modalFade 是否是淡入淡出 */PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), this.modalAppendToBody ? undefined : dom, props.modalClass, props.modalFade);// 如果设置了lock-scroll属性,默认为trueif (props.lockScroll) {// 这边的话 判断body是不是有 el-popup-parent--hiddenthis.withoutHiddenClass = !hasClass(document.body, 'el-popup-parent--hidden');if (this.withoutHiddenClass) {// 获取到 body的 padding-rightthis.bodyPaddingRight = document.body.style.paddingRight;this.computedBodyPaddingRight = parseInt(getStyle(document.body, 'paddingRight'), 10);}// getScrollBarWidth方法用来获取浏览器默认的滚动条宽度scrollBarWidth = getScrollBarWidth();// 判断body是否需要滚动let bodyHasOverflow = document.documentElement.clientHeight < document.body.scrollHeight;// 查看body overflowY 属性let 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';}// 给body添加el-popup-parent--hidden类名,该类名的样式为overflow: hidden; 从而实现将 body 滚动锁定addClass(document.body, 'el-popup-parent--hidden');}}// 如果dialog外层是没有定位的话那么就加上 absoluteif (getComputedStyle(dom).position === 'static') {dom.style.position = 'absolute';}// 给当前的dialog组件加上层级,层级的高度比遮罩高一层dom.style.zIndex = PopupManager.nextZIndex();this.opened = true;this.onOpen && this.onOpen();this.doAfterOpen();},doAfterOpen() {// _opening正在打开属性设为false, 打开弹框的流程就是这样了this._opening = false;},close() {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() {// _closing 正在关闭的属性设为truethis._closing = true;this.onClose && this.onClose();if (this.lockScroll) {setTimeout(this.restoreBodyStyle, 200);}this.opened = false;this.doAfterClose();},doAfterClose() {/** *PopupManager.closeModal 用来控制灰色遮罩的关闭 * @param String _popupId 弹窗的id */PopupManager.closeModal(this._popupId);// _closing 正在关闭的属性设为false, 关闭流程介绍this._closing = false;},restoreBodyStyle() {if (this.modal && this.withoutHiddenClass) {document.body.style.paddingRight = this.bodyPaddingRight;removeClass(document.body, 'el-popup-parent--hidden');}this.withoutHiddenClass = true;}}
};
export {PopupManager
};
打开packages/src/utils/popup/popup-manager.js重点分析下PopupManager.openModal和PopupManager.closeModal方法
import Vue from 'vue';
import { addClass, removeClass } from 'element-ui/src/utils/dom';
let hasModal = false;
let hasInitZIndex = false;
let zIndex;
// getModal方法用来生成弹框的灰色遮罩
// 遮罩只用生成一次,然后存到PopupManager.modalDom中,所有的弹框都用同一个遮罩
const getModal = function() {if (Vue.prototype.$isServer) return;let modalDom = PopupManager.modalDom;if (modalDom) {hasModal = true;} else {hasModal = false;modalDom = document.createElement('div');PopupManager.modalDom = modalDom;// 给遮罩绑定上touchmove事件modalDom.addEventListener('touchmove', function(event) {event.preventDefault();event.stopPropagation();});// 给遮罩绑定上click事件modalDom.addEventListener('click', function() {PopupManager.doOnModalClick && PopupManager.doOnModalClick();});}return modalDom;
};
const instances = {};
const PopupManager = {// 是否是淡入淡出modalFade: true,// 获取instance上的实例getInstance: function(id) {return instances[id];},// 往instance上的注册实例register: function(id, instance) {if (id && instance) {instances[id] = instance;}},// instance上的销毁实例deregister: function(id) {if (id) {instances[id] = null;delete instances[id];}},// 计算zIndex层级nextZIndex: function() {return PopupManager.zIndex++;},// 存储弹框的栈modalStack: [],// 执行对应弹框组件上的close方法doOnModalClick: function() {const topItem = PopupManager.modalStack[PopupManager.modalStack.length - 1];if (!topItem) return;const instance = PopupManager.getInstance(topItem.id);if (instance && instance.closeOnClickModal) {instance.close();}},// 该方法用来打开弹框的灰色遮罩openModal: function(id, zIndex, dom, modalClass, modalFade) {if (Vue.prototype.$isServer) return;if (!id || zIndex === undefined) return;// 这里要注意this指向,此时的this为PopupManager对象this.modalFade = modalFade;// 第一次这个栈 默认是个空的数组[]const modalStack = this.modalStack;// 遍历栈,找到对应id的弹框for (let i = 0, j = modalStack.length; i < j; i++) {const item = modalStack[i];if (item.id === id) {return;}}// 获取灰色遮罩的dom元素const modalDom = getModal();/* 给遮罩加上 v-modal类名,遮罩的半透明背景就是这样类名设置的.v-modal {position: fixed;left: 0;top: 0;width: 100%;height: 100%;opacity: 0.5;background: #000000;}*/addClass(modalDom, 'v-modal');if (this.modalFade && !hasModal) {// 加上v-modal-enter渐变的类名addClass(modalDom, 'v-modal-enter');}if (modalClass) {let classArr = modalClass.trim().split(/\s+/);classArr.forEach(item => addClass(modalDom, item));}// 200毫秒后去掉 v-modal-entersetTimeout(() => {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 已经遮罩的zIndex 存到modalStack中this.modalStack.push({ id: id, zIndex: zIndex, modalClass: modalClass });},// 用来关闭遮罩(存在同时打开多个弹框的情况)/** 1、根据id 获取对应的遮罩对象* 2、如果这个id就是modalStack最后一个,直接pop,并将遮罩的层级降为此时modalStack最后一个的层级* 3、如果modalStack.length === 0,也就是此时页面没有弹框了,将body上的modalDom移除,并PopupManager.modalDom = undefined* */closeModal: function(id) {// 获取modalStackconst modalStack = this.modalStack;const modalDom = getModal();if (modalStack.length > 0) {// 取出最后一个const topItem = modalStack[modalStack.length - 1];if (topItem.id === id) {// 如果有当前的这个有modalClass那么把这些个class都去掉if (topItem.modalClass) {let classArr = topItem.modalClass.trim().split(/\s+/);classArr.forEach(item => removeClass(modalDom, item));}// 最后一个删除掉modalStack.pop();// 还有的话,也就是同时打开多个弹框的情况if (modalStack.length > 0) {// 将遮罩的层级降为此时modalStack最后一个的层级// 一般来说就是流程是// 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) {// 加入淡入淡出的样式if (this.modalFade) {addClass(modalDom, 'v-modal-leave');}setTimeout(() => {if (modalStack.length === 0) {// 从body上移除遮罩,并重置PopupManager.modalDomif (modalDom.parentNode) modalDom.parentNode.removeChild(modalDom);modalDom.style.display = 'none';PopupManager.modalDom = undefined;}removeClass(modalDom, 'v-modal-leave');}, 200);}}
};
// 通过Object.defineProperty对PopupManager上的zIndex的拦截
// 第一次获取zIndex时,返回初始值为2000
Object.defineProperty(PopupManager, 'zIndex', {configurable: true,get() {if (!hasInitZIndex) {zIndex = zIndex || (Vue.prototype.$ELEMENT || {}).zIndex || 2000;hasInitZIndex = true;}return zIndex;},set(value) {zIndex = value;}
});
const getTopPopup = function() {if (Vue.prototype.$isServer) return;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 shownwindow.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;
总体来说,通过阅读dialog组件源码,感觉还是挺惊艳的。没想到一个小小的弹框组件,也是内有乾坤啊。弹框的流程控制、层级控制、组件之间的派发与广播、如何递归向上查找父组件、对.sync的运用、甚至是 e m i t 、 emit、 emit、on,这些都可以运用到平常的开发中。