美文网首页
解析elementui 中的v-loading指令

解析elementui 中的v-loading指令

作者: 冷暖自知_zjz | 来源:发表于2020-07-07 21:44 被阅读0次

    首先先学习一下指令的相关知识点

    钩子函数

    一个指令定义对象可以提供如下几个钩子函数 (均为可选):

    • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
    • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

    我们会在稍后讨论渲染函数时介绍更多 VNodes 的细节。

    • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
    • unbind:只调用一次,指令与元素解绑时调用。

    接下来我们来看一下钩子函数的参数 (即 elbindingvnodeoldVnode)。

    钩子函数参数

    • el:指令所绑定的元素,可以用来直接操作 DOM。
    • binding:一个对象,包含以下 property:
      • name:指令名,不包括 v- 前缀。
      • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
      • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
      • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
      • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
      • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }
    • vnode:Vue 编译生成的虚拟节点。移步 VNode API 来了解更多详情。
    • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

    剖析v-loading源码

    在工作中主要做pc端的后台系统,使用的ui库为elementui,相信使用vue的小伙伴对其并不陌生,下面我就对其中的v-loding的源码进行剖析

    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 {
            text: null,
            spinner: null,
            background: null,
            fullscreen: true,
            visible: false,
            customClass: ''
          };
        },
    
        methods: {
          handleAfterLeave() {
            this.$emit('after-leave');
          },
          setText(text) {
            this.text = text;
          }
        }
      };
    </script>
    
    

    directive.js

    import Vue from 'vue';
    import Loading from './loading.vue';
    // addClass为对应元素添加类名,removeClass删除对应的类名  getStyle获取对应的行内属性
    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';
    // 使用loading组件配置项创建vue子类构造函数
    const Mask = Vue.extend(Loading);
    
    const loadingDirective = {};
    loadingDirective.install = Vue => {
      // 是服务端渲染直接返回
      if (Vue.prototype.$isServer) return;
      const toggleLoading = (el, binding) => {
        if (binding.value) {
          Vue.nextTick(() => {
            // 一个包含修饰符的对象 eg:v-loading.fullscreen.lock="fullscreenLoading" 获取是否使用fullscreen操作符
            // 当使用指令方式时,全屏遮罩需要添加fullscreen修饰符(遮罩会插入至 body 上)
            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');
              // Loading 遮罩会插入到绑定元素的子节点,通过添加body修饰符,可以使遮罩插入至 DOM 中的 body 上
              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 {
          afterLeave(el.instance, _ => {
            if (!el.instance.hiding) return;
            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;
          // 表示loading组件是否隐藏
          el.instance.hiding = true;
        }
      };
      // 将对应的 loading组件渲染出来的dom插入到parent对应的dom上
      const insertDom = (parent, el, binding) => {
        // 若在前一个全屏 Loading 关闭前再次调用全屏 Loading,并不会创建一个新的 Loading 实例,而是返回现有全屏 Loading 的实例(当el.domVisible为true时说明loading组件已经插入到页面上了)
        if (!el.domVisible && getStyle(el, 'display') !== 'none' && getStyle(el, 'visibility') !== 'hidden') {
          // 将行内样式赋值到loading组件渲染出的dom上
          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;
    
          parent.appendChild(el.mask);
          Vue.nextTick(() => {
            if (el.instance.hiding) {
              el.instance.$emit('after-leave');
            } else {
              el.instance.visible = true;
            }
          });
          // 表示已经将对应的dom插入到页面中了
          el.domInserted = true;
        } else if (el.domVisible && el.instance.hiding === true) {
          el.instance.visible = true;
          el.instance.hiding = false;
        }
      };
    
      Vue.directive('loading', {
        bind: function(el, binding, vnode) {
          // 获取绑定指令元素对应元素的属性值,传入到loading组件的data中
          // 在绑定了v-loading指令的元素上添加element-loading-text属性,其值会被渲染为加载文案,并显示在加载图标的下方。类似地,element-loading-spinner和element-loading-background属性分别用来设定图标类名和背景色值
          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: {
              text: vm && vm[textExr] || textExr,
              spinner: vm && vm[spinnerExr] || spinnerExr,
              background: vm && vm[backgroundExr] || backgroundExr,
              customClass: vm && vm[customClassExr] || customClassExr,
              fullscreen: !!binding.modifiers.fullscreen
            }
          });
          // 将实例放到对应元素dom的instance上
          el.instance = mask;
          // 将实例loading组件渲染出来的dom放到mask属性上
          el.mask = mask.$el;
          // 存放loading渲染出来的dom属性的行内样式
          el.maskStyle = {};
          // 当值为true时调用
          binding.value && toggleLoading(el, binding);
        },
    
        update: function(el, binding) {
          el.instance.setText(el.getAttribute('element-loading-text'));
          if (binding.oldValue !== binding.value) {
            toggleLoading(el, binding);
          }
        },
    
        unbind: function(el, binding) {
          // domInserted是否插入到页面的标识,在insertDom方法中被赋值为true
          if (el.domInserted) {
            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;
    
    

    相关文章

      网友评论

          本文标题:解析elementui 中的v-loading指令

          本文链接:https://www.haomeiwen.com/subject/zrqtcktx.html