目录
以前在公司做组件封装,当时封装过message组件,对elementPlus的message组件也进行过研究。最近闲来无事,正好又看一下。
message方法
先看message方法,接收一个option参数,就是我们使用message方法时配置的内容
1. 首先对传来的参数进行处理
1.1 会对传来的配置进行判断,判断传来的字符串还是vnode还是正常的配置
字符串:
vnode:
正常配置:
1.2 normalized变量的normalized内容
1.3 判断是否有appendTo属性,没有则默认插入到body中,有则插入到指定DOM内
1.4 返回normalized这个处理后的变量
2. 判断是不是需要分组
2.1 查看配置处理后的对象中是否有grouping属性,并且实例数组不能为空
- 这里的实例数组,每次创建message,实例数组都会新增一个实例进去(除了grouping需要分组的,分组的只需要添加一次),先留意一下,下面会讲每个实例中都有那些内容
3. 开始创建message
const createMessage = ({ appendTo, ...options }) => {
const id = `message_${seed++}`
// 如果传入了onClose方法,就是传入的onClose,如果没传,则就是undefined。所以下面的用到了?.
const userOnClose = options.onClose
// 创建一个div元素
const container = document.createElement('div')
// 创建虚拟dom用,传给虚拟dom的props
const props: any = {
...options, // 用不到appendTo,所以上面进行了结构
id,
// 在 leave 钩子之前调用(message组件里面的Transition组件要用)
onClose: () => {
userOnClose?.() // 执行用户传入的onClose逻辑
closeMessage(instance) // 关闭message逻辑
},
// 在离开过渡完成、且元素已从 DOM 中移除时调用(message组件里面的Transition组件要用)
onDestroy: () => {
render(null, container)
},
}
// 创建虚拟DOM节点
const vnode = createVNode(
MessageConstructor, // 组件名
props, // props
// 插槽,如果是VNode用法,h函数自己就创建了,不需要生成虚拟DOM
isVNode(props.message) ? { default: () => props.message } : null
)
// 渲染成真实DOM(插入到上面创建的div中了)
render(vnode, container)
// 并插入到指定的DOM结构内(container内最外层只有一个div)
appendTo.appendChild(container.firstElementChild!)
// vnode.component就是生成这个虚拟DOM(message)元素
const vm = vnode.component!
// handler对象,存放close方法。共message组件消失的时候调用
const handler = {
close: () => {
// 把message组件往外抛出的visible变为false,组件内通过这个控制显示和隐藏(v-show)
vm.exposed!.visible.value = false
},
}
// 实例中存放的元素
const instance = {
id,
vnode,
vm,
handler,
props: (vnode.component as any).props,
}
// 返回实例
return instance
}
3.1 在外面需要一个seed变量,每次新增message,都让seed加1。刷新则重置
3.2 首先给每个message定义一个唯一的id
3.3 从剩余参数中拿出来onClose方法
3.4 创建div元素,一会生成的message组件就放在这个div里
3.5 创建vnode需要的props
- 这里的onClose和onDestroy就是message组件中Transition标签上用到的,先留意一下
- closeMessage方法
- 每个实例里面都会有handler对象,这个对象里面会有关闭的方法,留意一下
3.6 然后生成虚拟DOM并渲染真实DOM
- MessageConstructor就是message组件
3.7 插入到指定的DOM中去
3.8 把当前message组件实例获取过来,并赋值给vm
- 可以打印看一下vnode(其实就是vue的getCurrentInstance方法获取到的内容)
3.9 生成handler对象,并写入close方法
组件抛出部分:由于抛出的是整个ref对象,所以还保留着响应式
3.10 到这一步,就看到了组件实例存放的是什么了。有id,vnode,vm,handler,props
3.11 最后返回实例instance
4. 添加实例,并返回含有关闭方法的对象
message组件
- 看一下组件结构(这里我把引入的饿了吗组件都去掉了,换成了span)
<template>
<transition
:name="ns.b('fade')"
@before-leave="onClose"
@after-leave="$emit('destroy')"
>
<div
v-show="visible"
:id="id"
ref="messageRef"
:class="[
ns.b(),
{ [ns.m(type)]: type },
ns.is('center', center),
ns.is('closable', showClose),
ns.is('plain', plain),
customClass,
]"
:style="customStyle"
role="alert"
@mouseenter="clearTimer"
@mouseleave="startTimer"
>
<span v-if="repeatNum > 1" :class="ns.e('badge')">{{ repeatNum }} </span>
<span v-if="iconComponent">【{{ iconComponent }}】</span>
<slot>
<p v-if="!dangerouslyUseHTMLString" :class="ns.e('content')">
{{ message }}
</p>
<p v-else :class="ns.e('content')" v-html="message" />
</slot>
<span v-if="showClose" :class="ns.e('closeBtn')" @click.stop="close"
>close
</span>
</div>
</transition>
</template>
- 最外层使用Transition组件包裹,可以对内部v-show的div应用过渡效果。name属性命名,可以通过此命名来进行过渡或者动画效果,可以看官网说明:Transition | Vue.js (vuejs.org)
- 最主要的下面这两个事件,会在内部div隐藏的时候触发
- 但是发现当前页面这个事件只有一个,那使用的就是props传来的了
- onClose使用的是这里props传来的(上面有讲)
- 然后接着看下面的div
- 看下ns,是怎么来的
- 然后看下useNamespace方法,这里我直接把elementPlus的粘过来了。有兴趣的可以研究下
import { computed, getCurrentInstance, inject, ref, unref } from 'vue'
import type { InjectionKey, Ref } from 'vue'
export const defaultNamespace = 'el'
const statePrefix = 'is-'
const _bem = (
namespace: string,
block: string,
blockSuffix: string,
element: string,
modifier: string
) => {
let cls = `${namespace}-${block}`
if (blockSuffix) {
cls += `-${blockSuffix}`
}
if (element) {
cls += `__${element}`
}
if (modifier) {
cls += `--${modifier}`
}
return cls
}
export const namespaceContextKey: InjectionKey<Ref<string | undefined>> =
Symbol('namespaceContextKey')
export const useGetDerivedNamespace = (
namespaceOverrides?: Ref<string | undefined>
) => {
const derivedNamespace =
namespaceOverrides ||
(getCurrentInstance()
? inject(namespaceContextKey, ref(defaultNamespace))
: ref(defaultNamespace))
const namespace = computed(() => {
return unref(derivedNamespace) || defaultNamespace
})
return namespace
}
export const useNamespace = (
block: string,
namespaceOverrides?: Ref<string | undefined>
) => {
const namespace = useGetDerivedNamespace(namespaceOverrides)
const b = (blockSuffix = '') =>
_bem(namespace.value, block, blockSuffix, '', '')
const e = (element?: string) =>
element ? _bem(namespace.value, block, '', element, '') : ''
const m = (modifier?: string) =>
modifier ? _bem(namespace.value, block, '', '', modifier) : ''
const be = (blockSuffix?: string, element?: string) =>
blockSuffix && element
? _bem(namespace.value, block, blockSuffix, element, '')
: ''
const em = (element?: string, modifier?: string) =>
element && modifier
? _bem(namespace.value, block, '', element, modifier)
: ''
const bm = (blockSuffix?: string, modifier?: string) =>
blockSuffix && modifier
? _bem(namespace.value, block, blockSuffix, '', modifier)
: ''
const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
blockSuffix && element && modifier
? _bem(namespace.value, block, blockSuffix, element, modifier)
: ''
const is: {
(name: string, state: boolean | undefined): string
(name: string): string
} = (name: string, ...args: [boolean | undefined] | []) => {
const state = args.length >= 1 ? args[0]! : true
return name && state ? `${statePrefix}${name}` : ''
}
// for css var
// --el-xxx: value;
const cssVar = (object: Record<string, string>) => {
const styles: Record<string, string> = {}
for (const key in object) {
if (object[key]) {
styles[`--${namespace.value}-${key}`] = object[key]
}
}
return styles
}
// with block
const cssVarBlock = (object: Record<string, string>) => {
const styles: Record<string, string> = {}
for (const key in object) {
if (object[key]) {
styles[`--${namespace.value}-${block}-${key}`] = object[key]
}
}
return styles
}
const cssVarName = (name: string) => `--${namespace.value}-${name}`
const cssVarBlockName = (name: string) =>
`--${namespace.value}-${block}-${name}`
return {
namespace,
b,
e,
m,
be,
em,
bm,
bem,
is,
// css
cssVar,
cssVarName,
cssVarBlock,
cssVarBlockName,
}
}
export type UseNamespaceReturn = ReturnType<typeof useNamespace>
- 根据传入的props属性,来进行对应类名的添加
- 动态类名customStyle,主要是设置组件定位的top高度
getLastOffset方法
组件导出的bottom就是这项最终的top高度(组件本身的高度 + offset)
getOffsetOrSpace方法
这个bottom,如果是第一次添加message
不是第一次添加message
而元素的高度,又是怎么监听的呢?其实用到了useResizeObserver这个hook,页面一进来的时候,就会对指定的dom进行尺寸监听,只要尺寸变化了就会触发,第一次进来也会触发。这样就能得到元素最新的高度
然后接着往下看结构
一个是鼠标经过事件,一个是鼠标离开事件
- 分别控制清理定时和开始定时
- stopTimer有定义。开始定时的时候,会把结构出来的stop函数赋值给stopTimer
可以查看下useTimeoutFn的用法
这样的话,鼠标经过message,就会清理定时,鼠标离开message,就会重新开始计时
如果看下面逻辑的话,会发现它一进来就会调用计时,并显示message组件
然后如果鼠标不经过message,那么在设置时间过去后,就会调用close方法
组件v-show=false,就会关闭,就会触发Transition的这两个方法,并触发过渡动画效果
由于message组件是虚拟dom创建出来的,所以创建虚拟dom的props部分onClose和destory方法就会进行执行: 渲染函数 & JSX | Vue.js
执行onClose就会执行用户传入的onClose逻辑(如果用户传了的话),然后执行closeMessage方法。从实例数组中移除该项,并调用这项实例中关闭方法关闭(每项实例中都有handler对象,上面有讲)
执行onDestory方法,就是把message组件从结构中移除。onDestory执行时机要比onClose要晚一点
至此,一个mesage创建到销毁就执行完了
repeatNum部分
- repeatNum是props传来的。message组件也对它进行了侦听
如果变化了(上面message方法的2.1有讲repeatNum,只要从实例数组中查到了同样的显示内容,repeatNum就会+1)。首先开始清理定时,然后重新开始定时
- 结构部分,原先是el-badge组件,我给换成了span
其他部分
网盘地址
- 把主要逻辑都写里面了,样式没加,可以自己加一下。原有组件进行了删减,zIndex没有处理,还有键盘esc事件
链接:百度网盘 请输入提取码
提取码:ynbc