• 92409

    文章

  • 775

    评论

  • 17

    友链

  • 最近新加了换肤功能,大家多来逛逛吧~~~~
  • 喜欢这个网站的朋友可以加一下QQ群,我们一起交流技术。

element-ui之loading源码解析学习

撸了今年阿里、腾讯和美团的面试,我有一个重要发现.......>>

看Loading加载这里的使用指南可以知道,loading有2种使用方式。

一种是指令的方式:

<template>
  <el-table
    v-loading="loading">
  </el-table>
</template>

一种是服务的方式:

import { Loading } from 'element-ui';
let loadingInstance = Loading.service(options);
this.$nextTick(() => { // 以服务的方式调用的 Loading 需要异步关闭
  loadingInstance.close();
});

以服务的方式调用的全屏 Loading 是单例的:

et loadingInstance1 = Loading.service({ fullscreen: true });
let loadingInstance2 = Loading.service({ fullscreen: true });
console.log(loadingInstance1 === loadingInstance2); // true

如果完整引入了 Element,那么 Vue.prototype 上会有一个全局方法 $loading,它的调用方式为:this.$loading(options),同样会返回一个 Loading 实例。

以上这些都是指南上明确写出来的使用方式,这里只是照扒过来。目的是为了在学习代码的时候可以更加清晰地知道每段代码的用途,毕竟好多代码都是为了实现一些细节功能,看的时候不知道是干什么的,很容易懵逼。

了解了基本的用法之后,下面就正式开始源码的学习吧。

第一步,先看一下实现loading功能的代码结构:

loading目录下的index.js文件为入口文件:

import directive from './src/directive';
import service from './src/index';

export default {
  install(Vue) {
    Vue.use(directive);
    Vue.prototype.$loading = service;
  },
  directive,
  service
};

这个文件中的内容很少,主要就是用于统一导出各功能模块,就是文件入口该有的功能。

其中的directive是实现指令功能的模块,service用来实现服务的方式调用loading。

在分析指令和服务这两种调用方式的实现之前,先看一下这个loading.vue组件,毕竟loading.vue组件是loading功能的基础嘛,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>

写在全局的样式,el-loading-mask 

@include b(loading-mask) {
  position: absolute;
  z-index: 2000;
  background-color: rgba(255, 255, 255, .9);
  margin: 0;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  transition: opacity 0.3s;
}

这个组件写的很简单,主要就是loading的样式和一些判断逻辑。

简单说一下data中的变量,后续的代码中会用到。text用来展示loading那个转圈圈下面的文字提示,spinner为false的时候使用svg图来展示loading。

fullscreen为true的时候,组件dom插入到body中,使用全屏的样式is-fullscreen,让loading展示在屏幕的中央。具体样式代码如下,其实就是设置了position:fixed。

.el-loading-mask.is-fullscreen {
    position: fixed;
}

.el-loading-mask {
    position: absolute;
    z-index: 2000;
    background-color: rgba(255, 255, 255, 0.9);
    margin: 0;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    transition: opacity 0.3s;
}

visible用来控制loading的显示和隐藏。loading组件隐藏之后会触发after-leave自定义事件。

简单的了解了loading.vue组件之后,我们就先看一下service中的代码。

看下服务的方式是如何实现的

const Loading = (options = {}) => {
  if (Vue.prototype.$isServer) return;
  options = merge({}, defaults, options);
  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;
  }
  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;
  });
  if (options.fullscreen) {
    fullscreenLoading = instance;
  }
  return instance;
};

export default Loading;

上面的代码来自src/index.js,最终导出这个Loading函数。这个函数其实就是上面我们说到的Loading.service函数,通过执行这个函数,从而实现service调用loading的用法。

下面我们通过代码内添加的注释和代码外的解析,来逐行分析一下这个函数。 先看其中的一部分代码(可以跳过直接看串讲),如下:

import loadingVue from './loading.vue';
//导入loading组件

const LoadingConstructor = Vue.extend(loadingVue);
// 继承loading组件,得到一个构造函数,new这个构造函数产生一个新的loading组件实例

const defaults = {
  text: null,
  fullscreen: true,//默认设置为true,应该是想服务这种方式主要就是用来全屏覆盖的方式,就不用传了,直接默认
  body: false,
  lock: false,
  customClass: ''
};

const Loading = (options = {}) => {
//options变量用来接收外部传入的配置

if (Vue.prototype.$isServer) return;
//服务端渲染则不执行此函数

options = merge({}, defaults, options);
//options变量替换为一个默认配置合并了外部配置参数的新配置对象,要注意的是默认情况下fullscreen是true

if (typeof options.target === 'string') {
   options.target = document.querySelector(options.target);
}
//这里用来判断传入的target的参数,如果传入的target参数是字符串格式,则获取dom重新赋值给target。

//参数 target :
//Loading 需要覆盖的 DOM 节点。可传入一个 DOM 对象或字符串;若传入字符串,则会将其作为参数传
//入 document.querySelector以获取到对应 DOM 节点 

options.target = options.target || document.body;
//如果找不到target的dom,或者target为空的情况,默认遮盖dom。

if (options.target !== document.body) {
   //如果遮盖的目标不是body,那么不满足上述全屏遮盖的条件,把fullscreen也置为false
   options.fullscreen = false;
} else {
   //如果遮盖的目标是body,那么options.body置为true,loading插入到body
   options.body = true;
}

if (options.fullscreen && fullscreenLoading) {
  return fullscreenLoading;
}
//如果loading为全屏遮盖,并且fullscreenLoading变量中有值,就直接返回变量的值。fullscreenLoading这个变量是用来存储全局loading的实例的。
//这段代码就是为了实现全屏loading情况下的单例调用。我们在文章开始的地方介绍使用方式中提过,以服务的方式调用的全屏Loading 是单例的,就在这里实现。
//后面的代码会看到赋值fullscreenLoading的语句。

let parent = options.body ? document.body : options.target;
//这句其实就是根据上面的赋值进行判断了,设置parent变量,也就是loading插入的容器。

let instance = new LoadingConstructor({//相当于设置新的loading组件的挂在节点和data数据
    el: document.createElement('div'),
    data: options
});

...拆出部分代码放后面讲解
...拆出部分代码放后面讲解
...拆出部分代码放后面讲解
...拆出部分代码放后面讲解

if (options.fullscreen) {
   fullscreenLoading = instance;
//全局情况下把实例存了,实现单例
//全屏覆盖这种情况,整个页面就一个loading,单例操作合情合理
}

return instance;
}
看这部分代码,要事先搞清楚fullscreen、body和target三个参数被设计的意义,就很容易懂了。
直接上个思维导图

上图是我摸索的作者的设计思路,配合上图我们来串一下上面代码的逻辑,看看这段代码要干什么。(如果下面的逻辑有些细节不懂,就把上面加了注释的代码从上到下顺一遍)

那么,开始串一下上面代码的逻辑。代码一开始继承loading组件,得到一个构造函数,new这个构造函数产生一个新的loading组件实例。上面这些代码中的Loading函数部分,主要用来合成options对象,这个options对象的数据既用来传给new LoadingConstructor 里面的data,又作为状态机提供给后续代码判断使用。略去前面的合并对象和获取dom不讲。先是判断如果taget没传,那么默认就给target设置为document.body。如果这样,配合fullscreen默认为true,直接就全屏遮盖了,这也符合服务这种使用方式的主要意图,就是要优先全屏遮盖,单例调用。继续判断target参数,咱们这里只考虑target传入了有效值,target这里就有2种情况,是document.body或者不是。如果不是document.body,看上图第二个分支就知道肯定不满足全屏遮盖的条件了,因为fullscreen是默认为true的,要把fullscreen设置为false,否则样式就用了全屏遮盖样式了(fullscreen在options中传给data,赋值给loading组件中的fullscreen,用来控制全屏遮盖样式的使用,图上也有解释);如果是document.body,那么就走上图第一个分支设置body等于true了(这里之所以还要设置body这个感觉多次一举的操作,应该是后面会用到这个body来方便区分target的值),外加上默认的fullscreen为true,看第二个分支,就满足了全屏遮盖的条件。继续往下,如果fullscreen是true并且fullscreenLoading这个用来存全屏遮盖loading实例的变量有值,说明已经有存在的全屏遮盖loading实例了,那么直接返回这个实例,实现单例调用。判断body,把需要遮盖的目标赋值parent变量,loading组件的dom需要插入到这个parent容器中。将options传入到构造函数的参数中,new这个构造函数,返回一个loading实例,这个实例挂载在未插入页面的div上。

let instance = new LoadingConstructor({
   el: document.createElement('div'),
   data: options
});

LoadingConstructor是Vue.extend(loadingVue) 创建的一个loadingVue的“子类” ,new LoadingConstructor相当于创建了一个loadingVue组件的实例,el挂载的dom重写为一个创建的div元素,组件内部的data数据重写为options这个对象,得到的组件实例赋值给instance。

得到LoadingConstructor“类”的完整代码如下。

import loadingVue from './loading.vue';

let fullscreenLoading;

const LoadingConstructor = Vue.extend(loadingVue);

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

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

可以看到LoadingConstructor的原型链上添加了2个变量,originalPosition和originalOverflow,这两个变量在后续会用来存放loading所在容器的position和overflow样式。原型链上还添加了一个close函数用来关闭loading,这个函数等用到的时候再具体分析。

下面看上面的代码拆出的部分

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;
});
addStyle(options, parent, instance);

在instance这个loading组件的实例上添加一些样式,并存储一些样式,addStyle函数代码如下

const addStyle = (options, parent, instance) => {
  //声明一个空对象存储样式
  let maskStyle = {};
  if (options.fullscreen) {//如果是全屏遮盖
    在实例对象上存储body元素的position和overflow样式
    instance.originalPosition = getStyle(document.body, 'position');
    instance.originalOverflow = getStyle(document.body, 'overflow');
    //在maskstyle上面存储zindex
    maskStyle.zIndex = PopupManager.nextZIndex();
  } else if (options.body) {//如果只是遮盖body
    //实例上存储body的position样式
    instance.originalPosition = getStyle(document.body, 'position');
    //
    ['top', 'left'].forEach(property => {
      let scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
      //在maskstyle上存储target距离html相对于整个网页左上角的位置
      maskStyle[property] = options.target.getBoundingClientRect()[property] +
        document.body[scroll] +
        document.documentElement[scroll] +
        'px';
    });
    ['height', 'width'].forEach(property => {
      //在maskstyle上存储target的宽度和高度
      maskStyle[property] = options.target.getBoundingClientRect()[property] + 'px';
    });
  } else {//遮盖非document.body的target
    //在实例上存储parent的position样式
    instance.originalPosition = getStyle(parent, 'position');
  }
  Object.keys(maskStyle).forEach(property => {
    //maskstyle上存储的样式设置到loading的dom节点元素上
    instance.$el.style[property] = maskStyle[property];
  });
};

addStyle函数的主要作用就是通过添加样式帮助loading组件的遮罩层(最外层的div.el-loading-mask元素)定位,保证loading遮盖住target, 顺便存几个样式留着后面用。

当全屏遮盖的时候,由于fullscreen已经设置了fixed全屏样式,只要再添加一个zindex就好了。

当覆盖body的时候,作者考虑的比较多,如果body元素出滚动条即设置overflow = scroll,body因为要被遮盖也会被设置为position=relative。overflow所在的元素同时也是定位元素,里面的绝对定位元素会被裁剪。如果body的scrollLeft、scrollTop大于0,比如scrollLeft=200,那么loading就会被裁剪200px,页面上看到的效果就是loading整体左移了200px,那200px被隐藏到body之中。只有设置postion的left=200px,也就是加上body的scollLeft才可以正常显示loading。如果html也有scroll,那么就造成overflowe元素和绝对定位元素之间有定位元素(body)这种情况,也会被裁减,所以也要把html的scrollLeft和scrollTop的值加进去。然后,设置loading的宽度和高度和body元素的一样就可以了。

如果以上2中情况都不是,target是非body的元素,那么el-loading-mask的样式就可以满足定位了,不会出现什么问题,不需要这里的代码额外做什么。

addStyle函数里面还调用了一个getStyle函数,这是个工具函数,根据兼容性通过不同接口得到样式,看看就好了不做讲解,代码在/element-dev/src/utils/dom.js中,如下:

export const getStyle = ieVersion < 9 ? function(element, styleName) {
  if (isServer) return;
  if (!element || !styleName) return null;
  styleName = camelCase(styleName);
  if (styleName === 'float') {
    styleName = 'styleFloat';
  }
  try {
    switch (styleName) {
      case 'opacity':
        try {
          return element.filters.item('alpha').opacity / 100;
        } catch (e) {
          return 1.0;
        }
      default:
        return (element.style[styleName] || element.currentStyle ? element.currentStyle[styleName] : null);
    }
  } catch (e) {
    return element.style[styleName];
  }
} : function(element, styleName) {
  if (isServer) return;
  if (!element || !styleName) return null;
  styleName = camelCase(styleName);
  if (styleName === 'float') {
    styleName = 'cssFloat';
  }
  try {
    var computed = document.defaultView.getComputedStyle(element, '');
    return element.style[styleName] || computed ? computed[styleName] : null;
  } catch (e) {
    return element.style[styleName];
  }
};

继续往下看

if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') {
  addClass(parent, 'el-loading-parent--relative');
}

判断在addStyle函数中获取的originPosition的样式,如果不是absolute,也不是fixed,那么就在parent(target)上添加el-loading-parent--relative类,其实就是添加relative。

.el-loading-parent--relative {
  position: relative !important;
}

继续看代码 

if (options.fullscreen && options.lock) {
  addClass(parent, 'el-loading-parent--hidden');
}

这里判断如果是选择了fullscreen为true并且额外设置了lock属性为true,那么会全屏遮盖并且锁定屏幕的滚动。锁定屏幕的滚动就是通过给parent添加el-loading-parent--hidden类设置position:hidden来控制的,如下代码

.el-loading-parent--hidden {
  overflow: hidden !important;
}

继续向下看代码

parent.appendChild(instance.$el);
Vue.nextTick(() => {
  instance.visible = true;
});

把实例的dom节点插入到parent上,在页面上渲染出来。渲染之后 把组件中的visible置为true,loading从隐藏状态切换到显示状态,就大功告成了!

截止目前,使用服务的方式加载loading的代码分析完了,下面我们看看在服务这种方式下怎么关闭loading,代码如下

import afterLeave from 'element-ui/src/utils/after-leave';

LoadingConstructor.prototype.close = function() {
  //如果是全屏遮盖,将存储实例的变量置为undefined,致使无法再次调用存在的loading
  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;
};

这个close函数首先判断是否是全屏遮盖,如果是,就把存储单例的变量置空,然后调用afterLeave函数,执行afterLeave函数里的回调函数。这个回调函数写的很清楚简单,就是把target上添加的样式干掉,删除loading节点,调用loading组件的$destroy完全销毁实例,清理它与其它实例的连接,解绑它的全部指令及事件监听器。

这个afterLeave函数我们看看是具体干啥的,代码如下:

export default function(instance, callback, speed = 300, once = false) {
  if (!instance || !callback) throw new Error('instance & callback is required');
  let called = false;
  const afterLeaveCallback = function() {
    if (called) return;
    called = true;
    if (callback) {
      callback.apply(null, arguments);
    }
  };
  if (once) {
    instance.$once('after-leave', afterLeaveCallback);
  } else {
    instance.$on('after-leave', afterLeaveCallback);
  }
  setTimeout(() => {
    afterLeaveCallback();
  }, speed + 100);
};

这个函数就是设置了一个定时器,延时执行afterLeaveCallBack函数。这个afterLeaveCallBack函数只能执行一次,负责调用外传进来的callback函数。函数里面还做了一个操作,给实例绑定监听after-leave,$emit('after-leave')之后,执行监听函数afterLeaveCallBack。这个$emit('after-leave')在哪儿触发呢,其实是在loading.vue组件的代码里面。

<transition name="el-loading-fade" @after-leave="handleAfterLeave">

当loading组件隐藏的时候,会触发@after-leave这个自定义事件,然后在handleAfterLeave函数里面触发$emit('after-leave')。这部分代码在我们上面分析的服务调用这种方式中显然是用不上了,close方法就直接把loading的dom干掉了,轮不到隐藏的时候执行这个afterLeaveCallBack了。在下面我们即将要讲的指令方式下才会用到。

看下指令的方式是如何实现的

上面在服务方式中讲过的很多代码,在指令中都有很多类似的地方,所以指令部分的代码很多地方就不会讲的很细了。

指令的使用方式很简单,在文章一开始就已经举例了,就像下面这样。

<template>
  <el-table
    v-loading="loading">
  </el-table>
</template>

先把全部代码搞上来,代码如下,看着真是不少。

const loadingDirective = {};
loadingDirective.install = Vue => {
  if (Vue.prototype.$isServer) return;
  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 {
      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;
      el.instance.hiding = true;
    }
  };
  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;

      parent.appendChild(el.mask);
      Vue.nextTick(() => {
        if (el.instance.hiding) {
          el.instance.$emit('after-leave');
        } else {
          el.instance.visible = true;
        }
      });
      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) {
      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
        }
      });
      el.instance = mask;
      el.mask = mask.$el;
      el.maskStyle = {};

      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) {
      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;

这个代码最终是导出loadingDirective这个对象,一开头就是在loadingDirective对象中设置install函数,这种方式是为了导出之后按照vue的方式在全局绑定。这里面的自定义指令的函数Vue.directive才是主要的逻辑代码,如下:

Vue.directive('loading', {
    bind: function(el, binding, vnode) {
      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
        }
      });
      el.instance = mask;
      el.mask = mask.$el;
      el.maskStyle = {};

      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) {
      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();
    }
  });

先在vue手册里面截几张图,说明一下自定义指令中的bind、update、unbind以及他们参数的意义。

vue手册里面讲的已经很清楚了,下面我就不再解释了,直接去推进代码逻辑。

我们分部分来看代码,先看初始化的bind函数中的代码。

import Loading from './loading.vue';
const Mask = Vue.extend(Loading);

bind: function(el, binding, vnode) {
  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
    }
  });
  el.instance = mask;
  el.mask = mask.$el;
  el.maskStyle = {};

  binding.value && toggleLoading(el, binding);
},

代码实现的套路和服务那里是如出一辙呀,都是new一个新的loading实例,填写其中的data数据,挂载到一个没有插入到页面上的div上面。

手册里面说到“在绑定了v-loading指令的元素上添加element-loading-text属性,其值会被渲染为加载文案,并显示在加载图标的下方。类似地,element-loading-spinnerelement-loading-background属性分别用来设定图标类名和背景色值。” ,element-loading-custom-class用来填写loading 的自定义类名。上述这种用法就是通过el获取属性值,然后在编译作用域中找或者直接赋值到data中loading组件对应的变量中去而实现的。

然后将loading的实例存到el的instance属性上,loading的dom节点存到el的mask属性上,并在el上设置属性maskStyle用于存储样式。

如果binding.value也就是指令传入的值为true,那么执行toggleLoading函数,我们来看一下toggleLoading函数的代码。

  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 {
      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;
      el.instance.hiding = true;
    }
  };

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;

      parent.appendChild(el.mask);
      Vue.nextTick(() => {
        if (el.instance.hiding) {
          el.instance.$emit('after-leave');
        } else {
          el.instance.visible = true;
        }
      });
      el.domInserted = true;
    } else if (el.domVisible && el.instance.hiding === true) {
      el.instance.visible = true;
      el.instance.hiding = false;
    }
  };

这个函数里面的代码大部分都在服务那边出现过,不再详细的讲解了,应该都能看懂了,大概要注意几个地方简单说一下。

第一个,抽取出了insertDom函数,因为指令的方式调用loading不再是每次删除dom节点,而是通过显示隐藏来处理loading,这里需要处理的更加细腻,在insertDom里面加了很多变量用来处理状态来区分不同的情况。

第二个,非全屏遮盖body那里计算loading位置时,又额外减去了margin-left和margin-top,这是考虑的又全面了些。。。。

总之,toggleLoading就是处理loading在不同情况下的定位和如何显示隐藏问题。

update: function(el, binding) {
      el.instance.setText(el.getAttribute('element-loading-text'));
      if (binding.oldValue !== binding.value) {
        toggleLoading(el, binding);
      }
    },

unbind: function(el, binding) {
      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();
}

update这里就是切换loading的true和 false的时候执行的函数,也是执行toggleLoading,unbind函数就是在指令销毁的时候把loading处理干净哈哈。

 

至此,element-ui的Loading组件咱们就讲完了,指令部分没有细讲,但是如果有仔细看了服务的实现代码,这里基本上一看就懂了。

讲的不对的地方欢迎各位大佬评论区指正,谢谢!

 

 

 

 


695856371Web网页设计师②群 | 喜欢本站的朋友可以收藏本站,或者加入我们大家一起来交流技术!

欢迎来到梁钟霖个人博客网站。本个人博客网站提供最新的站长新闻,各种互联网资讯。 还提供个人博客模板,最新最全的java教程,java面试题。在此我将尽我最大所能将此个人博客网站做的最好! 谢谢大家,愿大家一起进步!

转载原创文章请注明出处,转载至: 梁钟霖个人博客www.liangzl.com

0条评论

Loading...


发表评论

电子邮件地址不会被公开。 必填项已用*标注

自定义皮肤
注册梁钟霖个人博客