相信你們確定都用過 element-ui 裏面的 v-loading
來寫加載,可是若是讓你來寫一個的話你會怎麼寫呢?javascript
衆所周知,element-ui 框架的 v-loading
有兩種使用方式,一種是在須要 loading 的標籤上直接使用 :v-loading='true'
,這種方式官方稱爲指令,還有一種就是使用 this.$loading(options)
來調用,這種方式官方稱之爲服務。html
人類對於對於美好的事物總會有趨向性,我也不外乎如此,話很少說(個屁,就你的話最多),直接扒源碼。前端
有些人天生就適合你,有的代碼天生就適合閱讀,優秀的開源項目都是如此,但願接下來個人解析也可讓你恍然大悟,隨後竊笑一聲,原來就這樣。vue
來回顧一下 v-loading
的指令使用方式java
<template>
<div :v-loading.fullscreen="true">全屏覆蓋</div>
</template>
複製代碼
再來看看服務的使用方式node
mounted() {
let loading = this.$loading({ fullscreen: true })
setTimeout(() => { loading.close() }, 1000)
}
複製代碼
稍微留點印象,咱們簡單粗暴一些,直接扒源碼吧。git
打開你的 node_modules
,目標 element-ui
,在 src
目錄下的 index.js
即是引入全部組件的地方,讓我看看今天是哪兩個小可愛要被咱們扒光光。github
// element-ui\src\index.js
// ...
// directive 指令裝載
Vue.use(Loading.directive)
// prototype 服務裝載
Vue.prototype.$loading = Loading.service
// ...
複製代碼
本着按部就班的原則,咱們先從使用較多的指令方式看起。面試
Vue.use()
這個指令是 Vue 用來安裝插件的,若是傳入的參數是一個對象,則該對象要提供一個 install
方法,若是是一個函數,則該函數被視爲 install
方法,在 install
方法調用時,會將 Vue
做爲參數傳入。算法
若是要了解更多有關 plugin,點擊這裏瞭解更多有關 plugin 的內容,尤大大的官方文檔簡直百讀不厭。
可是老闆!這 Loading 下的兩個是啥玩意兒呢!
來,咱們看這裏。
import directive from './src/directive'
import service from './src/index'
export default {
// 這裏爲何有個 install 呢
// 當你使用單組件單註冊的時候就會調用這裏了
// 效果和下面同樣,掛載指令,掛載服務
install(Vue) {
Vue.use(directive)
Vue.prototype.$loading = service
},
// 就是上面的 Loading.directive
directive,
// 就是上面的 Loading.service
service
}
複製代碼
接下來咱們終於要深刻源碼了!dokidoki...
v-loading
指令解析喝杯水,壓抑住激動的心情,咱們打開 packages
下的 loading\index.js
,能夠看到其對外曝露了 directive
指令,來,上路,咱們來看他的源碼。
看似有百來行的代碼,可是客官不要着急,我給你精簡一下,貼上主要代碼,爲了方便解說,在其中咱們只取 fullscreen
修飾詞。
import Vue from 'vue'
// 這裏就是咱們寫的比較多的單 .vue 文件了,不拓展開了
// 值得注意的是在這個單文件裏面的 data() {} 聲明的值
// 咱們下面會碰到
import Loading from './loading.vue'
// 老闆!Vue.extend() 是什麼!
// 代碼片斷以後我會簡單介紹
const Mask = Vue.extend(Loading)
const loadingDirective = {}
// 還記得 Vue.use() 的使用方法麼?若傳入的是對象,該對象須要一個 install 屬性
loadingDirective.install = Vue => {
// 這裏處理顯示、消失 loading
const toggleLoading = (el, binding) => {
// 若綁定值爲 truthy 則插入 loading 元素
// binding 值爲 directive 的幾個鉤子中會接受到的參數
if (binding.value) {
if (binding.modifiers.fullscreen) {
insertDom(document.body, el, binding)
}
// 否則則將其設爲不可見
// 從上往下讀咱們是第一次看到 visible 屬性
// 別急,往下看,這個屬性能夠其實就是單文件 loading.vue 裏面的
// data() { return { visible: false } }
} else {
el.instance.visible = false
}
}
const insertDom = (parent, el, binding) => {
// 將 loading 設爲可見
el.instance.visible = true
// appendChild 添加的元素若爲同一個,則不會重複添加
// 咱們 el.mask 所指的爲同一個 dom 元素
// 由於咱們只在 bind 的時候給 el.mask 賦值
// 而且在組件存在期間,bind 只會調用一次
parent.appendChild(el.mask)
}
// 在此註冊 directive 指令
Vue.directive('loading', {
bind: function(el, binding, vnode) {
// 建立一個子組件,這裏和 new Vue(options) 相似
// 返回一個組件實例
const mask = new Mask({
el: document.createElement('div'),
// 有些人看到這裏會迷惑,爲何這個 data 不按照 Vue 官方建議傳函數進去呢?
// 其實這裏二者皆可
// 稍微作一點延展好了,在 Vue 源碼裏面,data 是延遲求值的
// 貼一點 Vue 源碼上來
// return function mergedInstanceDataFn() {
// let instanceData = typeof childVal === 'function'
// ? childVal.call(vm, vm)
// : childVal;
// let defaultData = typeof parentVal === 'function'
// ? parentVal.call(vm, vm)
// : parentVal;
// if (instanceData) {
// return mergeData(instanceData, defaultData)
// } else {
// return defaultData
// }
// }
// instanceData 就是咱們如今傳入的 data: {}
// defaultData 就是咱們 loading.vue 裏面的 data() {}
// 看了這段代碼應該就不難理解爲何能夠傳對象進去了
data: {
fullscreen: !!binding.modifiers.fullscreen
}
})
// 將建立的子類掛載到 el 上
// 在 directive 的文檔中建議
// 應該保證除了 el 以外其餘參數(binding、vnode)都是隻讀的
el.instance = mask
// 掛載 dom
el.mask = mask.$el
// 若 binding 的值爲 truthy 運行 toogleLoading
binding.value && toggleLoading(el, binding)
},
update: function(el, binding) {
// 若舊不等於新值得時候(通常都是由 true 切換爲 false 的時候)
if (binding.oldValue !== binding.value) {
// 切換顯示或消失
toggleLoading(el, binding)
}
},
unbind: function(el, binding) {
// 當組件 unbind 的時候,執行組件銷燬
el.instance && el.instance.$destroy()
}
})
}
export default loadingDirective
複製代碼
Vue.extend
是什麼在平時的代碼中該方法咱們主動調用的很少,可是在咱們註冊組件的時候,好比,Vue.component('my-component', options)
,這個時候會自動調用 Vue.extend
,直接上源碼吧,該代碼是當調用 Vue.component()
的時候將會執行的。
ps:稍微作一下延展,對於 .vue
單文件,想必你們都能猜到是如何運做的了,首先會把 <template>
標籤的內容轉爲 render()
函數,說到這個 render()
函數,我又想安利一波 JSX 了(打住!),而後就接着走 Vue.component()
這條線路註冊組件了。
...
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
...
複製代碼
而在 extend
裏面又作了什麼事情呢?我很想直接貼上源碼再來分析,可是這樣就超出咱們今天要說的範圍了,一言以蔽之, Vue.extend
接受參數並返回一個構造器,new
該構造器能夠返回一個組件實例。
相信到了這裏你們已經對如何實現 v-loading
有了必定的瞭解了,勤快的小夥伴已經卷了袖子開幹了,可是文章還沒完,分析完指令模式咱們還要看看服務模式,稍做休息,上路。
點擊這裏瞭解更多有關 Vue.extend 的內容,依舊是官方文檔,Vue 的文檔我簡直吹爆(破音)。
若是打開開發者模式看過兩種 loading
的方式,應該會注意到指令模式和服務模式的區別,最直觀的就是如有 fullscreen
參數,指令模式下不會移除生成的 dom
元素,而在服務模式下會移除生成的 dom
元素。
個人廢話太多了,直接上源碼,一樣的,我會提取咱們須要關注的代碼片斷方便分析,有了指令模式的基礎,看服務模式的就沒什麼難度了。
import Vue from 'vue'
import loadingVue from './loading.vue'
// 和指令模式同樣,建立實例構造器
const LoadingConstructor = Vue.extend(loadingVue)
// 定義變量,若使用的是全屏 loading 那就要保證全局的 loading 只有一個
let fullscreenLoading
// 這裏能夠看到和指令模式不一樣的地方
// 在調用了 close 以後就會移除該元素並銷燬組件
LoadingConstructor.prototype.close = function() {
setTimeout(() => {
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
}
this.$destroy()
}, 3000)
}
const Loading = (options = {}) => {
// 若調用 loading 的時候傳入了 fullscreen 而且 fullscreenLoading 不爲 falsy
// fullscreenLoading 只會在下面賦值,而且指向了 loading 實例
if (options.fullscreen && fullscreenLoading) {
return fullscreenLoading
}
// 這裏就不用說了吧,和指令中是同樣的
let instance = new LoadingConstructor({
el: document.createElement('div'),
data: options
})
let parent = document.body
// 直接添加元素
parent.appendChild(instance.$el)
// 將其設置爲可見
// 另外,寫到這裏的時候我查閱了相關的資料
// 本身之前一直理解 nextTick 是在 dom 元素更新完畢以後再執行回調
// 可是發現可能並非這麼回事,後續我會繼續研究
// 若是乾貨足夠的話我會寫一篇關於 nextTick ui-render microtask macrotask 的文章
Vue.nextTick(() => {
instance.visible = true
})
// 若傳入了 fullscreen 參數,則將實例存儲
if (options.fullscreen) {
fullscreenLoading = instance
}
// 返回實例,方便以後可以調用原型上的 close() 方法
return instance
}
export default Loading
複製代碼
關於代碼裏的 fullscreenLoading
變量,根由 element-ui
官方的說明咱們應該能瞭解個大概,這是爲了保證覆蓋整個頁面的 loading 實例只有一個才存在的,官方文檔說明以下。
須要注意的是,以服務的方式調用的全屏 Loading 是單例的:若在前一個全屏 Loading 關閉前再次調用全屏 Loading,並不會建立一個新的 Loading 實例,而是返回現有全屏 Loading 的實例
至此文章主體內容已經結束了,看起來只是一個 v-loading
的功能但卻延伸出去不少的內容,我還精簡了不少代碼,因此我這只是管中窺豹很小的一部份內容,更多的內容推薦你們也去讀讀源碼,你會發現不同的世界。
有些人和我說,看源碼的時候一看開頭,十來個模塊引入,一看組件,百來行,瞬間就軟掉了,沒有看下去的慾望了,但怎麼說呢,這個時候我推薦能夠先從你有些瞭解的功能開始看起,好比 v-loading
,看到這個就知道是 directive
,再源碼裏看的時候關注關鍵點,好比 binding
、el
、vnode
,很快就能理清楚代碼的含義了。
若是身邊有更優秀的人那還好,能夠以他爲目標,可是當你一枝獨秀,身邊的人都不如本身,你不知本身該如何成長的時候,這個時候就應該去看看源碼,從源碼中學習,從源碼中提高本身。
該如何看待閱讀?一天的飯錢就能買到別人可能一生的心血,多麼值錢的買賣。--- 無名
雖然源碼可能不能稱做是一生的心血,可是想象一下,看源碼的過程當中就好像和做者面對面在交談,這個模塊怎麼安排,那個算法怎麼優化,看 Vue
的源碼更是了,好像面對面和尤大大在聊天,這這這,想一想就溼了啊!(眼角,爲何?由於太感動了)
這篇文章只是我在和你們分享一些優秀的人的心血,結合了一些本身的理解,但願你們看完整篇文章以後可以有一些收穫有一些沉澱,固然捲起袖子直接寫一個本身的 v-custom-loading
就更好了。
我在平時看源碼的過程當中會發現很多有意思的代碼,若是有興趣的話能夠來個人項目裏看看,裏面就有我本身寫的 v-custom-loading
,還結合了一些本身的想法,好比將組件實例掛載的時候,我推薦以下寫法。
const context = '@@loadingContext'
...
el[context] = { instance: mask }
...
複製代碼
這麼寫的緣由就是不要污染 el
元素自己有的屬性,畢竟有可能本身定義的屬性會和 dom
原有的屬性衝突。
另外我在 vue-element-admin 項目中提了 pr 用的就是這種寫法。具體能夠看 vue-element-admin issue 1704 和 vue-element-admin issue 1705。
ps:最近面試題氾濫,我想若是把標題改爲《面試題解析,你知道 v-loading 該如何實現麼?》會不會更有人關注一些?(狗頭保平安)
代碼即人生,我甘之如飴。
我在這裏 gayhub@jsjzh 歡迎你們來找我玩兒。
小夥伴們能夠直接加我或者加羣,咱們一塊兒學前端、看源碼、學算法,前端進階,加油。