Element-UI閱讀理解(4) - 彈窗管理工具類PopupManager

前言

不推薦通篇閱讀,建議將element-ui源碼下載並運行到本地,按照本身的方式閱讀源碼,趕上不明白點能夠來這裏Ctrl+F搜索一下,看這裏有沒有記錄。html

這算不上是分享,只是本身對源碼閱讀理解的記錄,請勿糾結行文雜亂(仍是菜菜,寫很差)vue

文章分兩部部分:

  • PopupManager的簡單介紹:以el-dialog 爲例,來講明彈窗組件與工具類的基本原理;node

  • 具體分析el-dialog的開關流程:包含對各個文件中的大部分的函數、生命週期函數、工具函數/類的具體分析,做爲記錄;(不推薦通篇閱讀,建議閱讀源碼時遇到不懂的點,能夠來查閱一下)git

popupManager的簡單介紹:

  • PopupManager以mixins的方式注入組件,擁有實例選項,props,data,watch,生命週期等等;
  • PopupManager是全部彈窗租件的公共代碼;
  • PopupManage的主體有兩個js文件構成:
    • index.js擁有實例選項,監聽visible並處理,在生命週期函數內有彈窗管理邏輯執行;
    • popup-manage.js index.js的專屬工具函數,管理彈窗;

以el-dialog 爲例,來理解彈窗組件與工具類github

須要注意的是: el-dialog並無使用PopupMangage的所有內容,這裏僅僅是以el-dialog爲切入點來理解PopupManageelement-ui

el-dialog只有component.vue一個文件,它監聽visible並處理組件內的事件,定義組件掛載dom操做和註銷生命週期,派發一些事件(與彈窗的管理無關);segmentfault

index.js在beforeMount,beforeDestory兩個生命週期內調用PopupManager的彈窗組管理相關的函數PopupManager中的register deregister closeModal函數;數組

index.js監聽visible,執行如下兩個流程:app

  • 打開彈窗: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>的添加和移除;dom

具體分析流程,開關彈窗(枯燥)

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

相關文章
相關標籤/搜索