最近在学习element-plus中的el-loading
loading在平时项目中用的也比较多,主要在为了发起请求并且等数据返回前这一段时间,让用户有一个好的体验
使用
1. 指令
<div
v-loading="loading"
element-loading-text="Loading..."
:element-loading-spinner="svg"
element-loading-svg-view-box="-10, -10, 50, 50"
element-loading-background="rgba(122, 122, 122, 0.8)"
style="height: 100px"></div>
<script lang="ts" setup>
const loading = ref(true)
</script>
使用指令v-loading,接收一个boolean值,通过改变boolean来控制loading显示隐藏
2. 服务
import { ElLoading } from 'element-plus'
const loadingInstance = ElLoading.service(options)
具体options可以查看官方文档
源码
源码入口在项目的packages/components/loading/index.ts文件下
入口
import { Loading } from './src/service'
import { vLoading } from './src/directive'
import type { App } from 'vue'
// installer and everything in all
export const ElLoading = {
install(app: App) {
app.directive('loading', vLoading)
app.config.globalProperties.$loading = Loading
},
directive: vLoading,
service: Loading,
}
可以看出使用指令是在./src/directive文件下,服务是在./src/service下
指令
在指令条件下,v-loading 接收一个boolean 值,如果想要改变其他属性,是直接添加attribute
入口
开始加载
export const vLoading = {
mounted(el, binding) {
if (binding.value) {
createInstance(el, binding)
}
}
如果binding.value为truthy值,开始执行createInstance函数,同时传入参数el,binding
createInstance
接收el(当前的绑定元素),binding(指令的参数),这两个实参
const INSTANCE_KEY = Symbol('ElLoading')
const createInstance = (el,binding)=>{
const options: LoadingOptions = {
....
text: getProp('text'),
background: getProp('background'),
target: getBindingProp('target') ?? (fullscreen ? undefined : el),
...
}
el[INSTANCE_KEY] = {
options,
instance: Loading(options),
}
}
其中的options 是做了一个整合,通过getProp和getBindingProp 获取Attribute和binding上的属性
target 如果不是用了fullscreen,那么target
就是el,即当前绑定的元素
同时在el身上绑定一个symbol属性,为了以后清除
主要是这个instance指向的loading(options)是重点
🚩 loading(options)
这个文件在./src/service,也是服务的用法的文件
cosnt Loading = (options)=>{
const resolved = resolveOptions(options)
const instance = createLoadingComponent({
...resolved,
closed: () => {
resolved.closed?.()
},
})
resolved.parent.appendChild(instance.$el)
// after instance render, then modify visible to trigger transition
// 同步更改 instance 中的 visible 属性
nextTick(() => (instance.visible.value = resolved.visible))
return instance
}
1. resolveOptions
在这个方法中使用resolveOptions对options进行模拟值处理
cosnt resolveOptions = (options) =>{
// 如果 传入的`options.target`是一个字符串的话
// 有可能是一个选择器
let target: HTMLElement
if (isString(options.target)) {
target =
document.querySelector<HTMLElement>(options.target) ?? document.body
} else {
target = options.target || document.body
}
return {
parent: target === document.body || options.body ? document.body : target,
...
background: options.background || '',
customClass: options.customClass || '',
//
visible: options.visible ?? true,
lock: options.lock ?? false,
target,
}
}
在指令形式中
-
不是
fullScreen的情况下,options.target是el也就是说parent是target -
如果是
fullScreen,options.target是document.body,parent是document.body
🚩 2. createLoadingComponent
传入解析后的options和一个closed 函数,生成实例instance
执行resolved.parent.appendChild(instance.$el)
把生成的真实DOM属性插入到resolved.parent上
export function createLoadingComponent(options) {
const data = reactive({
...options,
visible: false,
})
const elLoadingComponent = {
setup() {
return () => {
return h(
Transition,
{
name: ns.b('fade'),
onAfterLeave: handleAfterLeave,
},
{
default: withCtx(() => [
withDirectives(
createVNode(
'div',
{
style: {
backgroundColor: data.background || '',
},
class: [
ns.b('mask'),
data.customClass,
data.fullscreen ? 'is-fullscreen' : '',
],
},
[
h(
'div',
{
class: ns.b('spinner'),
},
[spinner, spinnerText]
),
]
),
[[vShow, data.visible]]
),
]),
}
)
}
},
}
const loadingInstance = createApp(elLoadingComponent)
const vm = loadingInstance.mount(document.createElement('div'))
// transition结束后 移除
function handleAfterLeave() {
destroySelf()
}
function destroySelf() {
vm.$el?.parentNode?.removeChild(vm.$el)
loadingInstance.unmount()
}
// 暴露给外界
function close() {
clearTimeout(afterLeaveTimer)
afterLeaveTimer = window.setTimeout(handleAfterLeave,400)
options.closed?.()
}
return {
// 保持响应式,后面可以更改
...toRefs(data),
close,
handleAfterLeave,
vm,
get $el() {
return vm.$el
},
}
重点:使用jsx语法描述函数elLoadingComponent,生成虚拟dom
h可以传入二个参数/三个参数
当传入三个参数的时候
- 第一个参数是
type,元素的标签类型 - 第二个参数是
attribute,元素的属性 - 第三个参数是
children,元素的子元素 由上述可知type是Transition,有一个class属性和一个onAfterLeave方法
第三个参数default,我看withCtx在vue3官网上并没有体现,而且去掉也没有影响
import { withDirectives,createVNode,vShow } from "vue";
{
default: () => [
withDirectives(
createVNode(
'div',
{
style: {},
class: [],
},
[
h(
'div',
{},
[spinner, spinnerText]
),
]
),
[[vShow, data.visible]]
),
],
}
尤其是 withDirectives,createVNode,vShow很少用到过
function withDirectives(
vnode: VNode,
directives: DirectiveArguments
): VNode
// [Directive, value, argument, modifiers]
type DirectiveArguments = Array<
| [Directive]
| [Directive, any]
| [Directive, any, string]
| [Directive, any, string, DirectiveModifiers]
>
接受两个参数,一个vnode,一个directives,其中
directives是一个二维数组,毕竟,一个元素身上有可能会有很多的指令,一个数组代表一对指令
example
import { h, withDirectives } from 'vue'
// a custom directive
const pin = {
mounted() {
/* ... */
},
updated() {
/* ... */
}
}
// <div v-pin:top.animate="200"></div>
const vnode = withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
h() 是超脚本的缩写 - 意思是"生成 HTML(超文本标记语言)的 JavaScript"。此名称继承自许多虚拟 DOM 实现共享的约定。更具描述性的名称可以是createVnode(),但是当您必须在渲染函数中多次调用此函数时,较短的名称会有所帮助。
createVNode 和 h本质是一眼的,只是createNode更加具有语义化
最后移除元素
当loading结束后,需要移除真实Dom元素和虚拟dom,外界使用
vm.$el?.parentNode?.removeChild(vm.$el)
loadingInstance.unmount()
总结
其实源码看下来,核心就那么几个
- 对用户传递的
options进行处理 - 生成一个虚拟dom 并且挂载
- 移除虚拟dom或者更新属性
// elLoadingComponent 虚拟dom,可以传入一个 .vue 文件
// createApp 第二个参数可以传参,当做 props
const loadingInstance = createApp(elLoadingComponent,{})
const vm = loadingInstance.mount(document.createElement('div'))
// 绑定的元素挂载
el.appendChild(vm.$el)
看完源码,自己实现了一个简单版
学到的知识点
- ts 方面
- 指令中的
Directive可以对el和binding
export const vLoading: Directive<ElementLoading, LoadingBinding> = {
mounted(el,binding){
}
}
- 单独设置
binding的ts类型
interface ElementLoading extends HTMLElement {
[INSTANCE_KEY]?: {
instance: LoadingInstance
options: LoadingOptions
}
}
const createInstance = (
el: ElementLoading,
binding: DirectiveBinding<LoadingBinding>
) => {}
- vue3 方面
使用
tsx写指令,接触到了withDirectives和createVNode
import { withDirectives,createVNode,vShow } from "vue";
{
default: () => [
withDirectives(
// 虚拟dom
createVNode(
'div',
{
style: {},
class: [],
},
[
h(
'div',
{},
[spinner, spinnerText]
),
]
),
// 指令数组
[[vShow, data.visible]]
),
],
}
如果是一个boolean值
- 在
laoding.ts中
const data = reactive({
...options,
visible: false,
})
const elLoadingComponent = {
return () => {
return h(Transition,{},default:()=>{
vnode,[[vShow,data.visible]]
})
}
return {
...toRefs(data)
}
}
- 外面使用
const instance = createLoadingComponent(option){
const resolved = resolveOptions(options)
// instance.visible
nextTick(() => (instance.visible.value = resolved.visible))
}
总体来说,比较简单,但是有一些思想还是挺好的