Vant 源码学习 - Popup 弹出层

678 阅读3分钟

Vant 源码学习 - Popup 弹出层

介绍

弹出层容器,用于展示弹窗、信息提示等内容,支持多个弹出层叠加展示。文档地址 跳转github

可以学会啥套路

  • createNamespace bem函数生成class computed属性计算style
  • useLazyRender控制组件是否懒加载
  • useGlobalZIndex 弹窗类的全局ZIndex控制,防止被遮盖
  • callInterceptor 拦截器对props.beforeClose函数进行拦截
  • Transition Teleport 内置组件

createNamespace bem函数生成class

BEM(块、元素、修饰符)是一种命名约定和方法论,用于在编写 CSS 或其他样式表时提供一种一致性和可维护性的方式。

BEM 的基本概念如下:

  1. 块(Block): 块代表一个独立的、可重用的组件或模块。一个块可以是页面上的任何元素,比如头部、导航栏、按钮等。块的类名应该使用单个单词或短划线分隔的多个单词,例如 .header、.navigation。

  2. 元素(Element): 元素是块的组成部分,具有依赖关系。元素的类名应该以块的类名作为前缀,使用双下划线连接,例如 .header__logo、.navigation__item。元素只在其父级块的上下文中有效,不应该单独使用。

  3. 修饰符(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 结构外层的位置去。