相信你們確定都用過 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 歡迎你們來找我玩兒。
小夥伴們能夠直接加我或者加羣,咱們一塊兒學前端、看源碼、學算法,前端進階,加油。