element-ui 源碼解析,你知道 v-loading 是如何實現的嗎?

vue

前言

相信你們確定都用過 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,再源碼裏看的時候關注關鍵點,好比 bindingelvnode,很快就能理清楚代碼的含義了。

若是身邊有更優秀的人那還好,能夠以他爲目標,可是當你一枝獨秀,身邊的人都不如本身,你不知本身該如何成長的時候,這個時候就應該去看看源碼,從源碼中學習,從源碼中提高本身。

該如何看待閱讀?一天的飯錢就能買到別人可能一生的心血,多麼值錢的買賣。--- 無名

雖然源碼可能不能稱做是一生的心血,可是想象一下,看源碼的過程當中就好像和做者面對面在交談,這個模塊怎麼安排,那個算法怎麼優化,看 Vue 的源碼更是了,好像面對面和尤大大在聊天,這這這,想一想就溼了啊!(眼角,爲何?由於太感動了)

這篇文章只是我在和你們分享一些優秀的人的心血,結合了一些本身的理解,但願你們看完整篇文章以後可以有一些收穫有一些沉澱,固然捲起袖子直接寫一個本身的 v-custom-loading 就更好了。

我在平時看源碼的過程當中會發現很多有意思的代碼,若是有興趣的話能夠來個人項目裏看看,裏面就有我本身寫的 v-custom-loading,還結合了一些本身的想法,好比將組件實例掛載的時候,我推薦以下寫法。

個人 vue-tiny-code 歡迎 star

const context = '@@loadingContext'
...
el[context] = { instance: mask }
...

這麼寫的緣由就是不要污染 el 元素自己有的屬性,畢竟有可能本身定義的屬性會和 dom 原有的屬性衝突。

另外我在 vue-element-admin 項目中提了 pr 用的就是這種寫法。具體能夠看 vue-element-admin issue 1704vue-element-admin issue 1705

ps:最近面試題氾濫,我想若是把標題改爲《面試題解析,你知道 v-loading 該如何實現麼?》會不會更有人關注一些?(狗頭保平安)

頁腳

代碼即人生,我甘之如飴。

我在這裏 gayhub@jsjzh 歡迎你們來找我玩兒。

小夥伴們能夠直接加我或者加羣,咱們一塊兒學前端、看源碼、學算法,前端進階,加油。

wechat

wechat

相關文章
相關標籤/搜索