Element 2 组件源码剖析之Loading 加载--服务实现

965 阅读4分钟

简介

本文将介绍组件的服务实现原理,耐心读完,相信会对您有所帮助。 为了更换的理解本文,请优先阅读下前文Loading指令实现

更多组件分析详见 👉 📚 Element UI 源码剖析组件总览

本专栏的 gitbook 版本地址已经发布 📚《learning element-ui》 ,内容同步更新中!

服务方式

前文中介绍了服务方式的使用方法Loading.service(options),文件packages\loading\src\index.js实现了该功能,其中很多的逻辑实现跟全文指令实现相同。

源码实现

源码简介后结构如下:

// packages\loading\src\index.js
// ...

// 返回一个扩展实例构造器(使用基础 Vue 构造器,创建一个“子类”)
const LoadingConstructor = Vue.extend(loadingVue);

// 默认配置
const defaults = {
  text: null, // 加载图标下方的加载文案
  fullscreen: true, // 默认全屏遮罩
  body: false, // 遮罩插入至 DOM 中的 body 上
  lock: false, // 全屏模式下锁定屏幕的滚动
  customClass: '' // 自定义类名
};

// 全屏遮罩实例缓存
let fullscreenLoading;

// 添加 originalPosition originalOverflow属性
LoadingConstructor.prototype.originalPosition = '';
LoadingConstructor.prototype.originalOverflow = '';

// 添加 close()  用于移除该元素并销毁组件
LoadingConstructor.prototype.close = function() {
  // ...
};

// 为loading组件以及插入父节点 添加样式
const addStyle = (options, parent, instance) => {
  // ...
};

// 返回组件实例
const Loading = (options = {}) => {
  // ...
  return instance;
};

export default Loading; 

使用Vue.extend 返回一个 loding 组件扩展实例构造器,用于实例的创建。同时为构造器对象添加了 originalPositionoriginalOverflow 属性和 close() 方法,方便实例的使用。

Loading()

组件使用service方法就是导出的 Loading,用于返回组件实例 ,options 参数配置项具体见 官方文档 options

const Loading = (options = {}) => {
  // Vue 实例是否运行于服务器
  if (Vue.prototype.$isServer) return;
  // 更新组件配置项
  options = merge({}, defaults, options);
  // Loading 需要覆盖的 DOM 节点
  if (typeof options.target === 'string') { 
    options.target = document.querySelector(options.target);
  } 
  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;
  }

  // 获取 loading 组件将要插入的父节点
  let parent = options.body ? document.body : options.target;
  // 创建组件实例
  let instance = new LoadingConstructor({
    el: document.createElement('div'),
    data: options
  });

  addStyle(options, parent, instance);
  // 根据父节点Position属性  决定父级是否需要增加relative属性
  if (instance.originalPosition !== 'absolute' && instance.originalPosition !== 'fixed') {
    addClass(parent, 'el-loading-parent--relative');
  }
  // 全屏模式下是否锁定屏幕滚动
  if (options.fullscreen && options.lock) {
    addClass(parent, 'el-loading-parent--hidden');
  }
  // 将loading实例附加到指定父节点的子节点列表的末尾
  parent.appendChild(instance.$el);
  Vue.nextTick(() => {
    // 显示组件
    instance.visible = true;
  });
  if (options.fullscreen) {
    // 全屏时 缓存组件实例
    fullscreenLoading = instance;
  }
  return instance;
};

根据初始配置项 const defaults = { fullscreen: true },可知使用服务方法时默认全屏加载,默认插入节点就是 body 元素,所以 参数target 默认值处理会有 document.body

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

 if (typeof options.target === 'string') { 
   options.target = document.querySelector(options.target);
 } 
 options.target = options.target || document.body;

对于全屏加载模式,target必须指定body 元素;否则认为时区域加载。

 // 遮罩插入节点不是 body 时,fullscreen 设置无效
  if (options.target !== document.body) {
    options.fullscreen = false;
  } else {
    options.body = true;
  } 

前文介绍服务方式使用时,提到Loading 是单例的:若在前一个全屏 Loading 关闭前再次调用全屏 Loading,并不会创建一个新的 Loading 实例,而是返回现有全屏 Loading 的实例。

const loadingInstance1 = this.$loading({ fullscreen: true }); 
const loadingInstance2 = this.$loading({ fullscreen: true }); 
console.log(loadingInstance1 === loadingInstance2); // true

实现逻辑如下,定了变量fullscreenLoading缓存全屏加载的遮罩组件实例,首次调用service方法返回 实例 instance,随后若是再次调用 service创建全屏组件,代码就直接返回 fullscreenLoading,这也是为什么前面提到的两个实例是同一个。

// 全屏遮罩实例缓存
let fullscreenLoading; 
const Loading = (options = {}) => {  
  // ...
  // 返回全屏遮罩实例
  if (options.fullscreen && fullscreenLoading) { 
    return fullscreenLoading;
  } 
  // ...
  if (options.fullscreen) {
    // 全屏时 缓存组件实例
    fullscreenLoading = instance;
  }
  return instance;
};

close()

方法 close 用于移除该组件实例的元素并销毁该实例 。

LoadingConstructor.prototype.close = function() {
  if (this.fullscreen) {
    fullscreenLoading = undefined;
  }
  afterLeave(this, _ => {
    console.log('afterLeave');
    // 判断父节点是否为 body
    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;
  console.log('visible afterLeave');
};

addStyle()

方法addStyle用于loading组件DOM操作(元素插入、样式更新)。

const addStyle = (options, parent, instance) => {
  // 遮罩样式
  let maskStyle = {};
  if (options.fullscreen) { // 全屏/整页模式
    // 获取body元素position属性值
    instance.originalPosition = getStyle(document.body, 'position');
    // 获取body元素overflow属性值
    instance.originalOverflow = getStyle(document.body, 'overflow');
    // 设置loading的DOM堆叠层级 z-index
    maskStyle.zIndex = PopupManager.nextZIndex();
  } else if (options.body) {
    // 区域加载非全屏/整页模式  指定组件插入节点为 body
    instance.originalPosition = getStyle(document.body, 'position');

    // 计算遮罩的left、top
    // 因为是区域加载 偏移量要基于绑定元素的left、top值
    ['top', 'left'].forEach(property => {
      let scroll = property === 'top' ? 'scrollTop' : 'scrollLeft';
      maskStyle[property] = options.target.getBoundingClientRect()[property] +
        document.body[scroll] +
        document.documentElement[scroll] +
        'px';
    });
    // 计算遮罩的 width 、height
    ['height', 'width'].forEach(property => {
      maskStyle[property] = options.target.getBoundingClientRect()[property] + 'px';
    });
  } else {
    // 绑定元素的父节点 position 属性
    instance.originalPosition = getStyle(parent, 'position');
  }
  // 遮罩组件样式绑定
  Object.keys(maskStyle).forEach(property => {
    instance.$el.style[property] = maskStyle[property];
  });
};

样式实现

组件样式源码 packages\theme-chalk\src\loading.scss 使用混合指令 bwhenm 嵌套生成组件样式。


@include b(loading-parent) {
  // 生成 .el-loading-parent--relative
  @include m(relative) {
    // ...
  } 
  // 生成 .el-loading-parent--hidden
  @include m(hidden) {
    // ...
  }
}
// 生成 .el-loading-mask
@include b(loading-mask) {
  // ...
  
  // 生成 .el-loading-mask.is-fullscreen
  @include when(fullscreen) {
    // ...
    
    // 生成 .el-loading-mask.is-fullscreen  .el-loading-spinner
    .el-loading-spinner {
      // ...
      
      // 生成.el-loading-mask.is-fullscreen .el-loading-spinner .circular
      .circular {
        // ...
      }
    }
  }
}

// 生成 .el-loading-spinner 
@include b(loading-spinner) {
  // ...
  
  // 生成 .el-loading-spinner .el-loading-text
  .el-loading-text {
    // ...
  }
  
  // 生成 .el-loading-spinner .circular
  .circular {
    // ...
    animation: loading-rotate 2s linear infinite;
  }
  // 生成 .el-loading-spinner .path
  .path {
    // ...
    animation: loading-dash 1.5s ease-in-out infinite; 
  }
  // 生成 .el-loading-spinner i
  i {
    // ...
  }
}
// 生成 .el-loading-fade-enter,.el-loading-fade-leave-active
.el-loading-fade-enter,
.el-loading-fade-leave-active {
  // ...
}
// 关键帧定义 loading-rotate
@keyframes loading-rotate {
  // ...
}
// 关键帧定义 loading-dash
@keyframes loading-dash {
  // ...
}

📚参考&关联阅读

Loading指令实现
'Vue-extend',vuejs

关注专栏

此文章已收录到专栏中 👇,可以直接关注。