Vant 源码学习 - Popup 弹出层
介绍
可以学会啥套路
createNamespacebem函数生成classcomputed属性计算styleuseLazyRender控制组件是否懒加载useGlobalZIndex弹窗类的全局ZIndex控制,防止被遮盖callInterceptor拦截器对props.beforeClose函数进行拦截TransitionTeleport内置组件
createNamespace bem函数生成class
BEM(块、元素、修饰符)是一种命名约定和方法论,用于在编写 CSS 或其他样式表时提供一种一致性和可维护性的方式。
BEM 的基本概念如下:
-
块(Block): 块代表一个独立的、可重用的组件或模块。一个块可以是页面上的任何元素,比如头部、导航栏、按钮等。块的类名应该使用单个单词或短划线分隔的多个单词,例如 .header、.navigation。
-
元素(Element): 元素是块的组成部分,具有依赖关系。元素的类名应该以块的类名作为前缀,使用双下划线连接,例如 .header__logo、.navigation__item。元素只在其父级块的上下文中有效,不应该单独使用。
-
修饰符(Modifier): 修饰符用于修改块或元素的外观、状态或行为。修饰符的类名应该以块或元素的类名作为前缀,使用单个短划线连接,例如 .button--primary、.header__logo--large。修饰符可以应用于块或元素,用于改变其样式或行为。
BEM 的命名约定提供了一种清晰的类名结构,使开发者能够快速理解和修改样式。它还鼓励使用嵌套选择器的方式编写样式,以确保样式的可重用性和可维护性。
const [name, bem] = createNamespace('popup');
// 组件name van-popup
export function createNamespace(name: string) {
const prefixedName = `van-${name}`;
return [
prefixedName,
createBEM(prefixedName),
createTranslate(prefixedName),
] as const;
}
// bem = createBEM('van-popup')
// bem('close-icon', 'top-right')
export function createBEM(name: string) {
return (el?: Mods, mods?: Mods): Mods => {
// 兼容 el 对象或者数组类型 赋值给 mods
if (el && typeof el !== 'string') {
mods = el;
el = '';
}
el = el ? `${name}__${el}` : name;
//genBem mods对象或者数组类型递归拼接成多个className
return `${el}${genBem(el, mods)}`;
};
}
//输出 van-popup__close-icon--top-right
useLazyRender控制组件是否懒加载
useLazyRender在首屏渲染和后续频繁触发切换效果更佳, 对比v-if在连续触发切换的场景下不用重新渲染,对比v-show在页面首次加载的场景下首屏加载效果更好
const lazyRender = useLazyRender(() => props.show || !props.lazyRender);
export function useLazyRender(show: WatchSource<boolean | undefined>) {
const inited = ref(false);
watch(
show,
(value) => {
if (value) {
inited.value = value;
}
},
{ immediate: true }
);
// 切换过程中 render 函数会重新渲染执行,但是render内部渲染函数通过 v-show 来缓存
return (render: () => JSX.Element) => () => inited.value ? render() : null;
}
const renderPopup = lazyRender(() => {
const { round, position, safeAreaInsetTop, safeAreaInsetBottom } = props;
return (
<div
v-show={props.show}
ref={popupRef}
style={style.value}
role="dialog"
tabindex={0}
class={[
bem({
round,
[position]: position,
}),
{
'van-safe-area-top': safeAreaInsetTop,
'van-safe-area-bottom': safeAreaInsetBottom,
},
]}
onKeydown={onKeydown}
{...attrs}
>
{slots.default?.()} // 默认插槽内容在renderPopup内部展示
{renderCloseIcon()}
</div>
);
});
useGlobalZIndex 弹窗类的全局ZIndex控制,防止被遮盖
- globalZIndex变量自动加加
// renderOverlay 和 renderTransition 都利用了useGlobalZIndex函数进行z-index++,renderTransition z-index更大所以就能展示出来
return (
<>
{renderOverlay()} // 遮罩层
{renderTransition()} // 展示内容
</>
);
// useGlobalZIndex
let globalZIndex = 2000;
/** the global z-index is automatically incremented after reading */
export const useGlobalZIndex = () => ++globalZIndex;
/** reset the global z-index */
export const setGlobalZIndex = (val: number) => {
globalZIndex = val;
};
callInterceptor 拦截器对props.beforeClose函数进行拦截
- 满足
props.beforeClose函数才触发关闭逻辑 callInterceptor兼容同步异步函数
// callInterceptor 调用
callInterceptor(props.beforeClose, {
done() {
opened = false;
emit('close');
emit('update:show', false);
},
});
// callInterceptor 函数
export const isPromise = <T = any>(val: unknown): val is Promise<T> =>
isObject(val) && isFunction(val.then) && isFunction(val.catch);
export function callInterceptor(
interceptor: Interceptor | undefined,
{
args = [],
done,
canceled,
}: {
args?: unknown[];
done: () => void;
canceled?: () => void;
}
) {
if (interceptor) {
// eslint-disable-next-line prefer-spread
const returnVal = interceptor.apply(null, args);
if (isPromise(returnVal)) {
returnVal
.then((value) => {
if (value) {
done();
} else if (canceled) {
canceled();
}
})
.catch(noop);
} else if (returnVal) {
done();
} else if (canceled) {
canceled();
}
} else {
done();
}
}
Transition 内置组件
是一个内置组件,这意味着它在任意别的组件中都可以被使用,无需注册。它可以将进入和离开动画应用到通过默认插槽传递给它的元素或组件上。进入或离开可以由以下的条件之一触发:
- 由 v-if 所触发的切换
- 由 v-show 所触发的切换
- 由特殊元素 切换的动态组件
- 改变特殊的 key 属性
Teleport 内置组件
是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去。