做爲一箇中後臺表單&表格工程師,常常須要在一個頁面中處理多個彈窗。我本身的項目中,一個複雜的審覈頁面中的彈窗數量超過了30個,如何管理大量的彈窗就成爲了一個須要考慮的問題。javascript
假設你有一個彈窗組件,相似於element-ui的Dialog,若是簡單粗暴的每個彈窗都寫一個dialog,那麼會有如下問題:vue
一個很容易想到的優化方法就是把一個彈窗做爲一個組件抽離出去,每一個彈窗的邏輯單獨寫在組件中。java
這樣經過組件拆分作很好的解決了模板過長的問題,也基本解決了命名困難的問題,不過仍是須要不少的變量去控制每一個組件的顯示。node
第一個辦法本質上並無減小重複的代碼和邏輯(彈窗顯示/關閉),只是把代碼放在了不一樣的文件當中。git
顯然,我並不須要寫那麼多的Dialog,Dialog自己並無變,做爲一個「包裹」組件,變的只是內容。github
因此,只須要寫一個dialog,配合Vue的動態組件Component,切換不一樣的組件就好了。element-ui
使用Component,咱們作到了一個頁面只須要一個Dialog,但其實整個網頁,也只須要一個全局的Dialog。babel
咱們在根組件下掛一個Dialog組件,組件內容依然使用動態component,組件的數據流轉,component傳遞等使用Vuex進行。app
做爲單個項目的解決方案,全局Dialog加動態Component其實已經足夠好了,使用一個函數調用就能夠顯示彈窗。框架
this.$dialog({ title: '我是彈窗', component: Test, props: { props }, // Test的props經過這樣傳遞 })
可是想要做爲通用解決方案,還不夠:
在我心中,一個理想的彈窗組件,須要是這樣的:
使用簡潔
this.$dialog({ title: '哎呀不錯哦', component: () => <Test onDone={ this.fetchData } name={ this.name }/> })
Let's go.
Vue做爲一個視圖層的框架,核心其實就是渲染函數,因此必定有一個辦法,能夠把一個Vue組件渲染成一個DOM,這個方法就是$mount。
// 這個Dialog組件就是寫好的彈窗組件 import Dialog from './Dialog' // dialog是一個單例,不須要重複建立 let dialog export default function createDialog(Vue, { store = {}, router = {} }, options) { if (dialog) { dialog.options = { ...options, } dialog.$children[0].visible = true } else { dialog = new Vue({ name: 'Root-Dialog', router, store, data() { return { options: { ...options }, } }, render(h) { return h(Dialog, { props: this.options, }) }, }) // 渲染出DOM並手動插入到body dialog.$mount() document.body.appendChild(dialog.$el) } // 暴露close方法 return { close: () => dialog.$children[0].close(), } }
基於element-ui的Dialog組件二次封裝,在原有的props以外,添加一個component,使用動態Component渲染上去就好了。
思路很簡單,可是有幾個問題須要考慮。
若是不作任何處理,當彈窗消失的時候component並不會銷燬;當再次顯示彈窗時,會傳入一個新的組件,這個時候,上一個組件才銷燬,這很是不合理。因此咱們須要在彈窗消失的時候手動銷燬傳入的component。
Vue的動態Component組件的is屬性接受的值有3種類型:
而咱們但願的調用形式裏,component是一個返回jsx的函數,而它會被babel插件babel-plugin-transform-vue-jsx轉換爲調用createElement函數的結果,也就是說
() => <Test >
這個函數最終返回的是一個Virtual Node。
而Vue的選項裏面,render最終返回的也是一個VNode。
也就是說,() => <Test >這個函數能夠做爲一個Vue組件的render選項,因此,咱們須要構造一個完整的Vue選項對象,而後將這個對象做爲動態component的is屬性,這樣就能夠渲染出這個Test組件了。
在這個過程當中,咱們能夠在這個Vnode裏面作一些有趣的事情,好比注入事件。
首先,這裏有一個剛需:彈窗內的組件須要能夠關閉彈窗,也就是它的父組件。
一般有兩個辦法能夠作到:
略微比較一下就能夠發現,拋出事件的方法優於回調函數的辦法(一般來講,「事件」都優於「回調」):
可是,拋出事件的實現卻要比傳入回調難不少,須要對VNode比較熟悉。
在Dialog組件內,咱們觸及不到組件的模板,因此簡單的在動態component模板上添加 @done 並不能完成事件監聽。由於事件監聽實際上是在render的過程當中進行的,而咱們的render是經過jsx的方式在調用$dialog函數時傳入的,因此只能手動在生成的VNode上添加事件監聽:
在 vNode.componentOptions.listeners中,添加咱們須要監聽的事件和事件處理函數:
let listeners = vNode.componentOptions.listeners if (!listeners) { listeners = {} vNode.componentOptions.listeners = listeners } // 添加done const orginDoneHandler = listeners.done listeners.done = function () { if (orginDoneHandler) orginDoneHandler() doneHandler() } // 添加cancel const orginCancelHandler = listeners.cancel listeners.cancel = function () { if (orginCancelHandler) orginCancelHandler() cancelHandler() }
在Dialog中,監聽了動態component的done和cancel事件,在任一事件觸發後都會關閉Dialog,組件$emit('done')表示完成了本身的業務,$emit('cancel)表示取消了本身的業務
到這裏,還有一個問題沒有解決:這個組件還不是響應式的,好比說,你在一個index組件中經過$dialog顯示一個彈窗
this.$dialog({ title: '響應式', component: () => <Test text={ this.text }/> })
當text更新時,彈窗中的內容並無更新,也就說,組件沒有從新渲染。
這裏就要涉及到一些Vue的原理了,好比說渲染流程,依賴收集,一兩句話也講不清楚,我試着大概的說一下:
首先,頁面上顯示的數據變了,必定是觸發了從新渲染,this.text = '新的text' 之因此會更新頁面,能夠理解爲一個渲染函數在this.text的setter中執行了。
那麼,this.text的getter怎麼樣才能知道要執行哪些函數,就是經過所謂的依賴收集。簡單來講,依賴收集是在渲染函數(渲染Vnode的函數)中進行的,在createElement中一旦經過this.text使用了這個變量,經過這個變量的getter就收集到了正在執行的渲染函數這一個依賴。
因此,粗暴的講,須要把this.text的訪問放在一個render函數(Vue選項對象的render)中進行。日常用的模板其實也是這樣,由於它最終都被Vue-loader編譯成了render。
_component() { // 這一步很重要,讓component收集到了這個計算屬性的依賴,不然當component變化時不會從新渲染組件 const fn = this.component let vNode // 返回vue選項對象 const that = this return { name: 'dynamic-wrapper', render() { // fn的運行必定要在render函數中,也是爲了掛載依賴 vNode = fn() ... } }
因此,這就是爲何必定要使用一個返回jsx的函數做爲,而不是直接美滋滋的使用jsx。由於,臣妾實在是作不到響應式呀~
this.$dialog({ title: '臣妾作不到啊~', component: <Text text={ this.text }/>, })
等於
// this.text的值爲text this.$dialog({ title: '臣妾作不到啊~', component: createElement( Text, props: { text: 'text', } ) })
完整代碼,拍着胸脯保證可用,已經在生產環境大量使用超過3個月的時間了。