不推薦通篇閱讀,建議將element-ui源碼下載並運行到本地,按照本身的方式閱讀源碼,趕上不明白點能夠來這裏Ctrl+F搜索一下,看這裏有沒有記錄。html
這算不上是分享,只是本身對源碼閱讀理解的記錄,請勿糾結行文雜亂(仍是菜菜,寫很差)vue
PopupManager的簡單介紹:以el-dialog 爲例,來講明彈窗組件與工具類的基本原理;node
具體分析el-dialog的開關流程:包含對各個文件中的大部分的函數、生命週期函數、工具函數/類的具體分析,做爲記錄;(不推薦通篇閱讀,建議閱讀源碼時遇到不懂的點,能夠來查閱一下)git
mixins
的方式注入組件,擁有實例選項,props,data,watch,生命週期等等;index.js
擁有實例選項,監聽visible並處理,在生命週期函數內有彈窗管理邏輯執行;popup-manage.js
index.js的專屬工具函數,管理彈窗;以el-dialog 爲例,來理解彈窗組件與工具類github
須要注意的是: el-dialog
並無使用PopupMangage
的所有內容,這裏僅僅是以el-dialog
爲切入點來理解PopupManage
element-ui
el-dialog只有component.vue一個文件,它監聽visible並處理組件內的事件,定義組件掛載dom操做和註銷生命週期,派發一些事件(與彈窗的管理無關);segmentfault
index.js在beforeMount,beforeDestory
兩個生命週期內調用PopupManager
的彈窗組管理相關的函數PopupManager
中的register deregister closeModal
函數;數組
index.js
監聽visible,執行如下兩個流程:app
函數close open
中分別定義_closeTimer _openTimer
兩個計時器,參數是延時配置項closeDelay
,做用是延時關閉組件,openDelay
同理;open,close
都方法各維護了,定時器的建立、清除,並執行真實開關組件的函數 doOpen,doClose
;openModal closeModal
維護彈窗組件的管理棧modalStack
,以及處理組件的遮罩層dom
(dom也在popup-manager.js中建立) 在<body>
的添加和移除;dom
// 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 = {..}
中的函數名便是函數的功能;
// 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;
}
}
複製代碼
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
,參考文檔
以上即是第一次打開彈窗的流程
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