vue 饿了么UI 组件loading分析

594 阅读2分钟

最近想努力一波 在学习下 饿了么UI组件 把一些常用的有价值的 都研究下

然后在这边 记录下

自己理解的不到位的地方 希望能得到各位大佬指点 或者互相交流下

今天分析一波 element ui 的loading 组件

ele loading组件解析

  • loading 基础的组件
  • 调用的方式有两种 一种是 服务式的
import { Loading } from 'element-ui';
Loading.service(options);
  • 一种的指令的
<el-button
  type="primary"
  @click="openFullScreen1"
  v-loading.fullscreen.lock="fullscreenLoading">
  指令方式
</el-button>
  • 先来分析下服务式的
// 这个就是 Loading.service(options)  调用的就是下面这个函数
const Loading = (options = {}) => {
  if (Vue.prototype.$isServer) return;
  // option 合并
  options = merge({}, defaults, options);
  // target  代表Loading 需要覆盖的DOM节点。
  if (typeof options.target === 'string') {
    options.target = document.querySelector(options.target);
  }
  // 如果没有的话默认就是插入到body
  options.target = options.target || document.body;
  // 如果是插入到body那么 fullscreen 就是全屏的了 
  if (options.target !== document.body) {
    options.fullscreen = false;
  } else {
    options.body = true;
  }
  // 如果都是全局的话 并且上一个还在的话  就不用重复在渲染了
  if (options.fullscreen && fullscreenLoading) {
    return fullscreenLoading;
  }
  // 挂载的目标元素
  const parent = options.body ? document.body : options.target;
  // const LoadingConstructor = Vue.extend(loadingVue);
  // 这个是继承了 loading 基础组件   返回一个构造函数
  // 传入 el 会让这个实例将立即进入编译过程  如果不传的话 就是要手动调用  new LoadingConstructor(...).$mount()
  const instance = new LoadingConstructor({
    el: document.createElement('div'),
    data: options,
  });
  // 增加样式  我们在来看下具体做了什么
  addStyle(options, parent, instance);
  // 假如外层不是 绝对定位也不是固定定位  那么就给要挂载的目标元素加上relative定位  让遮罩依照自身来定位
  if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') {
    addClass(parent, 'el-loading-parent--relative');
  }
  // 锁定外层   给外面加上 overflow: hidden !important;
  if (options.fullscreen && options.lock) {
    addClass(parent, 'el-loading-parent--hidden');
  }
  // 接着就是添加上 这个loading
  parent.appendChild(instance.$el);
  // 添加完 下一个事件环改变 visible  loading就显示出来了
  Vue.nextTick(() => {
    instance.visible = true;
  });
  // 给fullscreenLoading 赋值    单例模式
  if (options.fullscreen) {
    fullscreenLoading = instance;
  }
  return instance;
};

addStyle

const addStyle = (options, parent, instance) => {
  const maskStyle = {};
  // 是全屏的话 保留原先的 position overflow 样式
  if (options.fullscreen) {
    instance.originalPosition = getStyle(document.body, 'position');
    instance.originalOverflow = getStyle(document.body, 'overflow');
    // PopupManager 这个是ele组件的弹出层的一个管理类   后面有分析其他组件在说
    // 这个就是获取 下一次层级是多少
    maskStyle.zIndex = PopupManager.nextZIndex();
  } else if (options.body) {
    // 保存原来的 如果不是全屏显示  但是又要插入在body下面  那边就要去获取这个 宿主元素的位置了
    instance.originalPosition = getStyle(document.body, 'position');
    // 获取要挂载元素的左右位置 高宽   
    ['top', 'left'].forEach((property) => {
      const 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');
  }
  // 给当前这个loading外层设置上这些样式
  Object.keys(maskStyle).forEach((property) => {
    instance.$el.style[property] = maskStyle[property];
  });
};

接下来就是 close 关闭了

LoadingConstructor.prototype.close = function () {
  // 如果是全屏的话  就释放 fullscreenLoading 这个变量
  if (this.fullscreen) {
    fullscreenLoading = undefined;
  }
  // 来看下这个函数  是饿了么一个工具方法
  // 就是 transition组件动画 完成会触发一个事件 after-leave事件   这边让他监听这个事件
  afterLeave(this, (_) => {
    const target = this.fullscreen || this.body
      ? document.body
      : this.target;
    // 移除这两个class
    removeClass(target, 'el-loading-parent--relative');
    removeClass(target, 'el-loading-parent--hidden');
    if (this.$el && this.$el.parentNode) {
      // 移除这个组件的dom元素
      this.$el.parentNode.removeChild(this.$el);
    }
    // 销毁这个组件
    this.$destroy();
  }, 300);
  // 改变这个属性 loading自然关闭  关闭的后时候触发after-leave事件
  this.visible = false;
};

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) {
      // eslint-disable-next-line prefer-spread
      callback.apply(null, arguments);
    }
  };
  if (once) {
    instance.$once('after-leave', afterLeaveCallback);
  } else {
    instance.$on('after-leave', afterLeaveCallback);
  }
  // 这边应该防止没有触发after-leave   再次调用callback
  setTimeout(() => {
    afterLeaveCallback();
  }, speed + 100);
}

晚上继续分析下 指令方式的调用。。。 更新下

指令调用分析

  Vue.directive('loading', {
    bind(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');
      // 获取这些值 相当于就是 option的配置
      // 指的是当前调用这个指令的 VueComponent实例
      const vm = vnode.context;
      // const Mask = Vue.extend(Loading); 继承这个基础组件的构造函数
      // data 属性覆盖
      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(el, binding) {
      el.instance.setText(el.getAttribute('element-loading-text'));
      if (binding.oldValue !== binding.value) {
        toggleLoading(el, binding);
      }
    },
    // 移除元素  销毁这个组件
    unbind(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();
    },
  });

toggleLoading

const toggleLoading = (el, binding) => {
  // 如果是要显示的话 走这个
  if (binding.value) {
    Vue.nextTick(() => {
      /// binding.modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
        if (binding.modifiers.fullscreen) {
          // 如果是全屏的loading  
          // 记住原始的 position overflow  zIndex
          el.originalPosition = getStyle(document.body, 'position');
          el.originalOverflow = getStyle(document.body, 'overflow');
          el.maskStyle.zIndex = PopupManager.nextZIndex();
          // el.mask  Loading组件的$el   加上这个class
          addClass(el.mask, 'is-fullscreen');
          // 再来看下 insertDom 做了什么
          insertDom(document.body, el, binding);
      } else {
        // 不是全屏的话
        // 移除class
        removeClass(el.mask, 'is-fullscreen');
        if (binding.modifiers.body) {
          el.originalPosition = getStyle(document.body, 'position');
          // 但是插在baby上面  又不是全屏的话  相当于 你就要去获取这个 宿主元素在文档的位置了 以及宽高
          // 然后给这个 loading组件 外层元素给  设置上
          ['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
          insertDom(document.body, el, binding);
        } else {
          el.originalPosition = getStyle(el, 'position');
          // 指令当前元素
          insertDom(el, el, binding);
        }
      }
    });
  } else {
    // 这边是要离开了
    afterLeave(el.instance, (_) => {
      // 不是正在隐藏中 那边就直接return   因为可能消失过程中又显示了
      // 主要还是因为 afterLeave工具方法中有个 setTimeout 会过来执行 所以做个判断
      if (!el.instance.hiding) return;
      // 标识符设置为不可见
      el.domVisible = false;
      const target = binding.modifiers.fullscreen || binding.modifiers.body
        ? document.body
        : el;
      // 移除class
      removeClass(target, 'el-loading-parent--relative');
      removeClass(target, 'el-loading-parent--hidden');
      // 设置标识符
      el.instance.hiding = false;
    }, 300, true);
    // 设置 visible 为 false  那么就会隐藏起来
    el.instance.visible = false;
    // 正在隐藏 设置为true
    el.instance.hiding = true;
  }
};

insertDom

const insertDom = (parent, el, binding) => {
  // el 也就是这个 指令的宿主
  // el是显示的状态
  if (!el.domVisible && getStyle(el, 'display') !== 'none' && getStyle(el, 'visibility') !== 'hidden') {
    // 这边给 loading组件的 外层元素加上 这些style
    Object.keys(el.maskStyle).forEach((property) => {
      el.mask.style[property] = el.maskStyle[property];
    });
    // 宿主元素不是些定位那么就给宿主元素加上 reletive 定位
    if (el.originalPosition !== 'absolute' && el.originalPosition !== 'fixed') {
      addClass(parent, 'el-loading-parent--relative');
    }
    // 加上 overflow hidden
    if (binding.modifiers.fullscreen && binding.modifiers.lock) {
      addClass(parent, 'el-loading-parent--hidden');
    }
    el.domVisible = true;
    // 添加元素
    parent.appendChild(el.mask);
    Vue.nextTick(() => {
      // 标识符 el.instance.hiding  是否是隐藏状态
      if (el.instance.hiding) {
        el.instance.$emit('after-leave');
      } else {
        // 让loading显示出来
        el.instance.visible = true;
      }
    });
    // 标识符 loading 已插入
    el.domInserted = true;
  } else if (el.domVisible && el.instance.hiding === true) {
    // 这边就是说已经显示了  但是点了隐藏  然后又很快点了显示  也就是说离开过渡的动画还没走完 走这边的逻辑
    el.instance.visible = true;
    el.instance.hiding = false;
  }
};