element loading源碼


本文參考https://segmentfault.com/a/1190000018535744

loading/index.js

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
};

loading/src/loading.vue

<template>
  <transition name="el-loading-fade" @after-leave="handleAfterLeave">
    <div
      v-show="visible"
      class="el-loading-mask"
      :style="{ backgroundColor: background || '' }"
      :class="[customClass, { 'is-fullscreen': fullscreen }]">
      <div class="el-loading-spinner">
        <svg v-if="!spinner" class="circular" viewBox="25 25 50 50">
          <circle class="path" cx="50" cy="50" r="20" fill="none"/>
        </svg>
        <i v-else :class="spinner"></i>
        <p v-if="text" class="el-loading-text">{{ text }}</p>
      </div>
    </div>
  </transition>
</template>

<script>
  export default {
    data() {
      return {
        // 顯示在加載圖標下方的加載文案    string
        text: null,
        // 自定義加載圖標類名    string
        spinner: null,
        // 遮罩背景色    string
        background: null,
        // 同 v-loading 指令中的 fullscreen 修飾符    boolean
        fullscreen: true,
        // 是否顯示
        visible: false,
        // customClass    Loading 的自定義類名    string
        customClass: ''
      };
    },

    methods: {
      // loading結束后觸發
      handleAfterLeave() {
        this.$emit('after-leave');
      },
      setText(text) {
        this.text = text;
      }
    }
  };
</script>

loading/src/directive.js

// v-loading 指令解析
import Vue from 'vue';
import Loading from './loading.vue';
import { addClass, removeClass, getStyle } from 'element-ui/src/utils/dom';
import { PopupManager } from 'element-ui/src/utils/popup';
import afterLeave from 'element-ui/src/utils/after-leave';
const Mask = Vue.extend(Loading);

const loadingDirective = {};
// 還記得 Vue.use() 的使用方法么?若傳入的是對象,該對象需要一個 install 屬性
loadingDirective.install = Vue => {
  if (Vue.prototype.$isServer) return;
  // 這里處理顯示、消失 loading
  const toggleLoading = (el, binding) => {
    if (binding.value) {
      Vue.nextTick(() => {
        if (binding.modifiers.fullscreen) {
          el.originalPosition = getStyle(document.body, 'position');
          el.originalOverflow = getStyle(document.body, 'overflow');
          el.maskStyle.zIndex = PopupManager.nextZIndex();

          addClass(el.mask, 'is-fullscreen');
          insertDom(document.body, el, binding);
        } else {
          removeClass(el.mask, 'is-fullscreen');

          if (binding.modifiers.body) {
            el.originalPosition = getStyle(document.body, 'position');

            ['top', 'left'].forEach(property => {
              const scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
              el.maskStyle[property] = el.getBoundingClientRect()[property] +
                document.body[scroll] +
                document.documentElement[scroll] -
                parseInt(getStyle(document.body, `margin-${property}`), 10) +
                'px';
            });
            ['height', 'width'].forEach(property => {
              el.maskStyle[property] = el.getBoundingClientRect()[property] + 'px';
            });

            insertDom(document.body, el, binding);
          } else {
            el.originalPosition = getStyle(el, 'position');
            insertDom(el, el, binding);
          }
        }
      });
    } else {
      // 不然則將其設為不可見
      // 從上往下讀我們是第一次看到 visible 屬性
      // 別急,往下看,這個屬性可以其實就是單文件 loading.vue 里面的
      // data() { return { visible: false } }
      afterLeave(el.instance, _ => {
        el.domVisible = false;
        const target = binding.modifiers.fullscreen || binding.modifiers.body
          ? document.body
          : el;
        removeClass(target, 'el-loading-parent--relative');
        removeClass(target, 'el-loading-parent--hidden');
        el.instance.hiding = false;
      }, 300, true);
      el.instance.visible = false;
      el.instance.hiding = true;
    }
  };
  // 插入dom
  const insertDom = (parent, el, binding) => {

    if (!el.domVisible && getStyle(el, 'display') !== 'none' && getStyle(el, 'visibility') !== 'hidden') {
      Object.keys(el.maskStyle).forEach(property => {
        el.mask.style[property] = el.maskStyle[property];
      });

      if (el.originalPosition !== 'absolute' && el.originalPosition !== 'fixed') {
        addClass(parent, 'el-loading-parent--relative');
      }
      if (binding.modifiers.fullscreen && binding.modifiers.lock) {
        addClass(parent, 'el-loading-parent--hidden');
      }
      el.domVisible = true;
      // appendChild 添加的元素若為同一個,則不會重復添加
      // 我們 el.mask 所指的為同一個 dom 元素
      // 因為我們只在 bind 的時候給 el.mask 賦值
      // 並且在組件存在期間,bind 只會調用一次
      parent.appendChild(el.mask);
      Vue.nextTick(() => {
        if (el.instance.hiding) {
          el.instance.$emit('after-leave');
        } else {
          // 將 loading 設為可見
          el.instance.visible = true;
        }
      });
      el.domInserted = true;
    }
  };
  // 在此注冊 directive 指令
  Vue.directive('loading', {
    bind: function (el, binding, vnode) {
      // 創建一個子組件,這里和 new Vue(options) 類似
      // 返回一個組件實例
      const textExr = el.getAttribute('element-loading-text');
      const spinnerExr = el.getAttribute('element-loading-spinner');
      const backgroundExr = el.getAttribute('element-loading-background');
      const customClassExr = el.getAttribute('element-loading-custom-class');
      const vm = vnode.context;
      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: {
          text: vm && vm[textExr] || textExr,
          spinner: vm && vm[spinnerExr] || spinnerExr,
          background: vm && vm[backgroundExr] || backgroundExr,
          customClass: vm && vm[customClassExr] || customClassExr,
          fullscreen: !!binding.modifiers.fullscreen
        }
      });
      // 將創建的子類掛載到 el 上
      // 在 directive 的文檔中建議
      // 應該保證除了 el 之外其他參數(binding、vnode)都是只讀的
      el.instance = mask;
      // 掛載 dom
      el.mask = mask.$el;
      el.maskStyle = {};
      // 若 binding 的值為 truthy 運行 toogleLoading
      binding.value && toggleLoading(el, binding);
    },

    update: function (el, binding) {
      el.instance.setText(el.getAttribute('element-loading-text'));
      if (binding.oldValue !== binding.value) {
        // 若舊不等於新值得時候(一般都是由 true 切換為 false 的時候)
        toggleLoading(el, binding);
      }
    },

    unbind: function (el, binding) {
      if (el.domInserted) {
        // 移除dom
        el.mask &&
          el.mask.parentNode &&
          el.mask.parentNode.removeChild(el.mask);
        toggleLoading(el, { value: false, modifiers: binding.modifiers });
      }
      el.instance && el.instance.$destroy();
    }
  });
};

export default loadingDirective;

loading/src/index.js

// loading服務模式
import Vue from 'vue';
import loadingVue from './loading.vue';
import { addClass, removeClass, getStyle } from 'element-ui/src/utils/dom';
import { PopupManager } from 'element-ui/src/utils/popup';
import afterLeave from 'element-ui/src/utils/after-leave';
import merge from 'element-ui/src/utils/merge';

// 和指令模式一樣,創建實例構造器
const LoadingConstructor = Vue.extend(loadingVue);

const defaults = {
  text: null,
  fullscreen: true,
  body: false,
  lock: false,
  customClass: ''
};
// 定義變量,若使用的是全屏 loading 那就要保證全局的 loading 只有一個
let fullscreenLoading;

LoadingConstructor.prototype.originalPosition = '';
LoadingConstructor.prototype.originalOverflow = '';

// 這里可以看到和指令模式不同的地方
// 在調用了 close 之后就會移除該元素並銷毀組件
LoadingConstructor.prototype.close = function () {
  if (this.fullscreen) {
    fullscreenLoading = undefined;
  }
  afterLeave(this, _ => {
    const target = this.fullscreen || this.body
      ? document.body
      : this.target;
    removeClass(target, 'el-loading-parent--relative');
    removeClass(target, 'el-loading-parent--hidden');
    if (this.$el && this.$el.parentNode) {
      this.$el.parentNode.removeChild(this.$el);
    }
    this.$destroy();
  }, 300);
  this.visible = false;
};

const addStyle = (options, parent, instance) => {
  let maskStyle = {};
  if (options.fullscreen) {
    instance.originalPosition = getStyle(document.body, 'position');
    instance.originalOverflow = getStyle(document.body, 'overflow');
    maskStyle.zIndex = PopupManager.nextZIndex();
  } else if (options.body) {
    instance.originalPosition = getStyle(document.body, 'position');
    ['top', 'left'].forEach(property => {
      let scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
      maskStyle[property] = options.target.getBoundingClientRect()[property] +
        document.body[scroll] +
        document.documentElement[scroll] +
        'px';
    });
    ['height', 'width'].forEach(property => {
      maskStyle[property] = options.target.getBoundingClientRect()[property] + 'px';
    });
  } else {
    instance.originalPosition = getStyle(parent, 'position');
  }
  Object.keys(maskStyle).forEach(property => {
    instance.$el.style[property] = maskStyle[property];
  });
};

const Loading = (options = {}) => {
  if (Vue.prototype.$isServer) return;
  options = merge({}, defaults, options);
  // Loading 需要覆蓋的 DOM 節點。可傳入一個 DOM 對象或字符串;若傳入字符串,則會將其作為參數傳入 document.querySelector以獲取到對應 DOM 節點
  if (typeof options.target === 'string') {
    options.target = document.querySelector(options.target);
  }
  options.target = options.target || document.body;
  if (options.target !== document.body) {
    options.fullscreen = false;
  } else {
    options.body = true;
  }
  // 若調用 loading 的時候傳入了 fullscreen 並且 fullscreenLoading 不為 falsy
  // fullscreenLoading 只會在下面賦值,並且指向了 loading 實例
  if (options.fullscreen && fullscreenLoading) {
    return fullscreenLoading;
  }

  let parent = options.body ? document.body : options.target;
  // 這里就不用說了吧,和指令中是一樣的
  let instance = new LoadingConstructor({
    el: document.createElement('div'),
    data: options
  });

  addStyle(options, parent, instance);
  if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') {
    addClass(parent, 'el-loading-parent--relative');
  }
  if (options.fullscreen && options.lock) {
    addClass(parent, 'el-loading-parent--hidden');
  }
  // 直接添加元素
  parent.appendChild(instance.$el);
  Vue.nextTick(() => {
    instance.visible = true;
  });
  // 若傳入了 fullscreen 參數,則將實例存儲
  if (options.fullscreen) {
    // 需要注意的是,以服務的方式調用的全屏 Loading 是單例的:若在前一個全屏 Loading 關閉前再次調用全屏 Loading,並不會創建一個新的 Loading 實例,而是返回現有全屏 Loading 的實例
    fullscreenLoading = instance; 
  }
  return instance;
};

export default Loading;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM