el-loading

1,840 阅读2分钟

最近在学习element-plus中的el-loading
loading在平时项目中用的也比较多,主要在为了发起请求并且等数据返回前这一段时间,让用户有一个好的体验

element-uigit地址

动画.gif

使用

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可以查看官方文档

image.png

源码

源码入口在项目的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.valuetruthy值,开始执行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 是做了一个整合,通过getPropgetBindingProp 获取Attributebinding上的属性

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

在这个方法中使用resolveOptionsoptions进行模拟值处理

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

在指令形式中

  1. 不是fullScreen的情况下,options.targetel 也就是说parenttarget

  2. 如果是fullScreen,options.targetdocument.bodyparentdocument.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可以传入二个参数/三个参数 当传入三个参数的时候

  1. 第一个参数是type,元素的标签类型
  2. 第二个参数是attribute,元素的属性
  3. 第三个参数是children,元素的子元素 由上述可知 typeTransition,有一个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很少用到过

  1. withDirectives
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 }] 
])
  1. createVNode

image.png

h() 是超脚本的缩写 - 意思是"生成 HTML(超文本标记语言)的 JavaScript"。此名称继承自许多虚拟 DOM 实现共享的约定。更具描述性的名称可以是createVnode(),但是当您必须在渲染函数中多次调用此函数时,较短的名称会有所帮助。

createVNodeh本质是一眼的,只是createNode更加具有语义化

最后移除元素

loading结束后,需要移除真实Dom元素和虚拟dom,外界使用

vm.$el?.parentNode?.removeChild(vm.$el)
loadingInstance.unmount()

总结

其实源码看下来,核心就那么几个

  1. 对用户传递的options进行处理
  2. 生成一个虚拟dom 并且挂载
  3. 移除虚拟dom或者更新属性
 // elLoadingComponent 虚拟dom,可以传入一个 .vue 文件
 
 // createApp 第二个参数可以传参,当做 props 
   const loadingInstance = createApp(elLoadingComponent,{})
  const vm = loadingInstance.mount(document.createElement('div'))
  // 绑定的元素挂载 
 el.appendChild(vm.$el)

看完源码,自己实现了一个简单版

学到的知识点

  1. ts 方面
  • 指令中的Directive可以对elbinding
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>
 ) => {}
  1. vue3 方面 使用tsx 写指令,接触到了withDirectivescreateVNode
import { withDirectives,createVNode,vShow } from "vue";
{
   default: () => [
              withDirectives(
              // 虚拟dom
                createVNode(
                  'div',
                  {
                    style: {},
                    class: [],
                  },
                  [
                    h(
                      'div',
                      {},
                      [spinner, spinnerText]
                    ),
                  ]
                ),
                // 指令数组
                [[vShow, data.visible]]
              ),
            ],   
}

如果是一个boolean

  1. laoding.ts
    const data = reactive({
    ...options,
    visible: false,
  })
  
  const elLoadingComponent = { 
   return () => {
       return h(Transition,{},default:()=>{
           vnode,[[vShow,data.visible]]
       })
   }
   return {
       ...toRefs(data)
   }
  }
  1. 外面使用
const instance = createLoadingComponent(option){
    const resolved = resolveOptions(options)
    // instance.visible
    nextTick(() => (instance.visible.value = resolved.visible))
}

总体来说,比较简单,但是有一些思想还是挺好的