完全搞懂elementUI指令與服務模式原理

不甘作輪子的搬運工!!!

記錄一個實習菜鳥寫圖片預覽組件的艱辛道路~html


elementUI不少組件中使用了指令模式和服務模式,好比: loadingmessage...

如下以loading組件爲例:

指令模式:

<template>
  <div :v-loading.fullscreen="true">全屏覆蓋</div>
</template>

服務模式:

const loading = this.$loading({
  lock: true,
  text: 'Loading',
  spinner: 'el-icon-loading',
  background: 'rgba(0, 0, 0, 0.7)'
});

跟大多數萌新同樣,啥是服務?!


  • 先看看elmentUI的目錄結構:

打開node_modules目錄,找到其下elementUI目錄:vue

clipboard.png

element-ui\src\index.js文件中有一大坨組件註冊信息,重點找到咱們要找的loading...node

// ...
// directive 指令裝載
Vue.use(Loading.directive)
// prototype 服務裝載
Vue.prototype.$loading = Loading.service
// ...

Vue.use() 這個指令是 Vue 用來安裝插件的,若是傳入的參數是一個對象,則該對象要提供一個 install 方法,若是是一個函數,則該函數被視爲 install 方法,在 install 方法調用時,會將 Vue 做爲參數傳入。git

開始叭!

先看看loading/index.js文件中是什麼鬼?
clipboard.pnggithub

//引入指令文件和服務文件,directive爲指令模式文件,index.js爲服務模式文件
import directive from './src/directive';
import service from './src/index';

export default {
  //install方法註冊組件,不在贅述install的用法,star-pic-list圖片預覽組件文章中已經介紹過
  install(Vue) {
    Vue.use(directive);
    //在vue的原型對象上註冊一個$loading的對象,這個$loading很是眼熟,看上面服務模式的使用,用到了this.$loading,源頭找到了
    Vue.prototype.$loading = service;
  },
  //引入的directive文件
  directive,
  //引入的index.js文件
  service
};

v-loading 指令解析

篇幅太長,其中咱們只取 fullscreen 修飾詞。element-ui

// 引入 .vue 文件
import Vue from 'vue'
// 引入loading.vue基礎文件,裏面包含的是組件的基礎結構,如html結構,loading顯示的頁面結構都在這裏面
import Loading from './loading.vue'
// 後面重點講解extend()構造器
// Vue.extend() 是vue構造器,它返回的是一個擴展實例構造器,也就是預設了部分選項的Vue實例構造器,

// mask字面意思是面具,掩飾,能夠猜出來,這個經過Vue.extend(Loading)返回構造器應該是用於咱們loading加載時的遮罩層用的
// loading就是預設選項,就像vue示例中,有components,name,data,methods...好像有點明白了
const Mask = Vue.extend(Loading)

const loadingDirective = {}
// 還記得 Vue.use() 的使用方法麼?若傳入的是對象,該對象須要一個 install 屬性
loadingDirective.install = Vue => {
  // toggleLoading 方法看名字就是切換loading顯示和隱藏的嘛~
  const toggleLoading = (el, binding) => {
    // 若綁定值爲 truthy 則插入 loading 元素
    // binding 值是一個對象,有指令名、指令的綁定值、modifiers修飾符對象等等等等,具體的能夠去了解自定義指令相關內容
    if (binding.value) {  //binding.value是綁定的指令值
      if (binding.modifiers.fullscreen) {  還記得咱們插入的指令嗎?:v-loading.fullscreen="true" ,  .fullscreen就是修飾符
        insertDom(document.body, el, binding)    //insertDom看名字就知道是插入新的元素
      }
     // visible 是loading.vue   data裏面定義的值
    } else {
      el.instance.visible = false
    }
  }

  const insertDom = (parent, el, binding) => {
    // loading 設爲可見
    el.instance.visible = true
    // appendChild 添加的元素若爲同一個,則不會重複添加
    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
      // bind 只會調用一次,在bind 的時候給 el.mask 賦值,所以el.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
關於extend()更多內容請參考 這裏,很是通熟易懂!

loading服務方式調用原理

直接看源碼:app

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

現學現用-寫一個點擊圖片預覽的組件試試看

目錄結構:

directive.js是指令模式文件,index.js是服務模式文件,star-pic-preview.vue是基礎單文件,包含了基礎的html結構dom

clipboard.png

先看指令模式:

  • 使用:

我直接使用了指令,並無傳參,由於功能簡單,默認參數就是false函數

<img
    src="http://img5.imgtn.bdimg.com/it/u=3300305952,1328708913&fm=26&gp=0.jpg"
    v-pic-preview
 >
  • 效果:

點擊出現遮罩層,圖片居中顯示預覽ui

clipboard.png

服務模式:

  • 使用:
<img
    src="http://img4q.duitang.com/uploads/item/201502/22/20150222191447_jdBYa.thumb.700_0.jpeg"
    @click="openImagePreview2"
>
methods: {
    // 服務方式
    openImagePreview2(e) {
        // 若是隻傳圖片
        this.$picPreview(e.target.src);
        //若是傳複雜對象,能夠配置遮罩層的背景顏色等...
        // this.$picPreview({
        //     background: 'rgba(0, 0, 0, 0.7)',
        //     imageUrl: e.target.src,
        // });
    },
}
  • 效果如上

源碼請參考github地址: 源碼地址

相關文章
相關標籤/搜索