最近想努力一波 在学习下 饿了么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;
}
};