2.5 Trigger源码分析 -- ant-design-vue系列

news2024/11/14 14:04:23

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

1 概述

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

在源码的实现中,Trigger组件主要有两个作用:

  1. 使用Portal组件,把Popup组件传送到指定的dom下,默认是body
  2. target节点绑定事件,控制事件的触发逻辑。

2 极简实现

为了实现以上功能,我们可以和源码一样,使用vue3提供的Teleport组件,来实现节点的传送;同时把所有事件进行透传即可。

在这里trigger就是我们原先的target节点,可以翻译成切换器。

setup(props, { slots }) {
		const align: any = computed(() => {
			const { placement } = props;

			return placements[placement];
		});

		const getComponent = () => {
			return (
				<Popup
					style={{ position: 'absolute' }}
					target={() => triggerRef.value!}
					align={align.value}
					visible={props.visible}
				>
					{slots.popup?.()}
				</Popup>
			);
		};

		const triggerRef = ref<HTMLElement>();

		return () => {
      // 1 Popup 部分
			const portal = <Portal>{getComponent()}</Portal>;
      // 2 target部分                    
			const trigger = (
				<div style={{ display: 'inline-block' }} ref={triggerRef}>
					{slots.default?.()}
				</div>
			);

			return (
				<>
					{portal}
					{trigger}
				</>
			);
		};
	}

3 源码分析

3.1 整体结构

在这里插入图片描述

这个组件比较特殊,使用了选项式的写法。

export default defineComponent({
  name: 'Trigger',
  mixins: [BaseMixin],
  inheritAttrs: false, // 用于控制组件的根元素是否应该继承父作用域中的属性(attribute)和事件监听器(listener)。
  porps: {},
  setup() {}, // 使用props提供的响应式变量,这里是 定位&portal 相关的;并且声明了一些初始值
  data() (), // 处理visible变量,为this挂载所有事件,尝试让PopupRef变量指向Portal
  watch: (), // 监听visible的变化
  created() {}, // 依赖注入,提供vcTriggerContext和PortalContextKey上下文
  deactivated() {}, // 组件失活时,关闭popup弹窗
  mounted() {}, // 调用updatedCal(),这个函数的作用是在visible为true的时候,注册点击/滚动/失焦的相关事件,以便于在点击popup外部/页面滚动/窗口失焦的时候关闭弹窗;在visible为false时,移除事件监听。
  updated() {}, // 组件属性更新后调用updatedCal(),重新注册。
  beforeUnmount() {}, // 卸载前清除所有监听器
  methods: {}, // 事件的执行、事件是否绑定、获取组件的方法等
  render() {} // 渲染trigger和portal
})

3.2 render函数

const child = children[0];来看,代码默认使用第一个子节点,所以调用的时候最好只传入一个子节点。

render() {
    const { $attrs } = this;
    const children = filterEmpty(getSlot(this));
    const { alignPoint } = this.$props;

    const child = children[0];
    this.childOriginEvents = getEvents(child);
    const newChildProps: any = {
      key: 'trigger',
    };
		
  	/**
  	* 这里有各种事件,其他删除,以click为例
  	*/
    if (this.isClickToHide() || this.isClickToShow()) {
      newChildProps.onClick = this.onClick;
      newChildProps.onMousedown = this.onMousedown;
      newChildProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] = this.onTouchstart;
    } else {
      newChildProps.onClick = this.createTwoChains('onClick');
      newChildProps.onMousedown = this.createTwoChains('onMousedown');
      newChildProps[supportsPassive ? 'onTouchstartPassive' : 'onTouchstart'] =
        this.createTwoChains('onTouchstart');
    }

  	/**
  	* 这个函数内部是vue3提供的cloneVNode实现的
  	*/
    const trigger = cloneElement(child, { ...newChildProps, ref: 'triggerRef' }, true, true);
    if (this.popPortal) {
      return trigger;
    } else {
      const portal = (
        <Portal
          key="portal"
          v-slots={{ default: this.getComponent }}
          getContainer={this.getContainer}
          didUpdate={this.handlePortalUpdate}
        ></Portal>
      );
      return (
        <>
          {portal}
          {trigger}
        </>
      );
    }
  },
  1. 找到Trigger组件包裹的所有非空子节点,取出第一个子节点child,把child上注册的事件收集起来,挂到childOriginEvents属性上。👑 节点为空的判断如下:

    c.type === Comment || (c.type === Fragment && c.children.length === 0) ||(c.type === Text && c.children.trim() === '')
    
  2. child节点挂上一些新的属性。以click事件为例,如果action中包含click事件,那么调用者就是希望点击的时候触发这个事件:也就是说isClickToHide或者isClickToShowtrue,那么直接把传给Trigger组件的click事件给child挂上。

    isClickToHide 判断如下:

    /**
    * action 和 hideAction都是数组,假设action=['click', 'hover'],那么 isClickToHide 就是 true
    */
    isClickToHide() {
      const { action, hideAction } = this.$props;
      return action.indexOf('click') !== -1 || hideAction.indexOf('click') !== -1;
    },
    

    在这里插入图片描述

  3. 如果isClickToHideisClickToShow都是false,那么调用this.createTwoChains('onClick')。这个函数模拟了“事件冒泡”的过程,因为原来的层级节点已经不存在了,但是绑定的事件不能丢失。

    具体做法是:如果第一个子节点和Trigger组件都有click事件,那么给child挂上的新属性就是fireclick,调用的时候会依次触发两个click事件(如下图);如果不是都有,那么哪个有就执行哪个;如果一个都没有,就执行空函数。代码如下:

    createTwoChains(event: string) {
      let fn = () => {};
      const events = getEvents(this);
      if (this.childOriginEvents[event] && events[event]) {
        return this[`fire${event}`];
      }
      fn = this.childOriginEvents[event] || events[event] || fn;
      return fn as any;
    },
      
    fireEvents(type: string, e: Event) {
      if (this.childOriginEvents[type]) {
        this.childOriginEvents[type](e);
      }
      const event = this.$props[type] || this.$attrs[type];
      if (event) {
        event(e);
      }
    },
    

    在这里插入图片描述

  • Portal组件中,container并不是body,而是一个div,这是通过getContainer={this.getContainer}实现的,看一下这个函数的实现。

    生成一个新的div,设置为absolute定位,保证popup不会导致滚动条出现。

getContainer() {
  const { $props: props } = this;
  const { getDocument } = props;
  const popupContainer = getDocument(this.getRootDomNode()).createElement('div');

  popupContainer.style.position = 'absolute';
  popupContainer.style.top = '0';
  popupContainer.style.left = '0';
  popupContainer.style.width = '100%';
  this.attachParent(popupContainer);
  return popupContainer;
},
  • Portal组件中,Popup一定会注册onMousedown事件,对应以下第一段代码。根据条件会注册onMouseenter或者onMouseleave事件,对应以下第二段代码。
/**
* 执行的是vcTriggerContext的方法
*/
onPopupMouseDown(...args: any[]) {
  // ......
  if (vcTriggerContext.onPopupMouseDown) {
    vcTriggerContext.onPopupMouseDown(...args);
  }
},

onPopupMouseenter只清除回调;onPopupMouseleave会在延迟后关闭弹窗。我们可以调整延迟时间,达到如下效果:如果当鼠标离开后,再次快速进入,那么关闭弹窗的回调就会被取消。

在这里插入图片描述

/**
* delayTimer是requestAnimationTimeout的执行器,作用是在delay时间后的requestAnimationFrame中执行回调
* clearDelayTimer 是取消掉回调的执行。
*/
onPopupMouseenter() {
  this.clearDelayTimer();
}

/**
* 在延迟后关闭。
* relatedTarget指向与当前事件相关的元素,包括焦点、悬停和其他事件
*/
onPopupMouseleave(e) {
  if (
    e &&
    e.relatedTarget &&
    !e.relatedTarget.setTimeout &&
    contains(this.popupRef?.getElement(), e.relatedTarget)
  ) {
    return;
  }
  this.delaySetPopupVisible(false, this.$props.mouseLeaveDelay);
}

/**
* 如果delayS是0,直接修改状态;否则在延迟结束后的requestAnimationFrame中执行回调
*/
delaySetPopupVisible(visible: boolean, delayS: number, event?: any) {
  const delay = delayS * 1000;
  this.clearDelayTimer();
  if (delay) {
    const point = event ? { pageX: event.pageX, pageY: event.pageY } : null;
    this.delayTimer = requestAnimationTimeout(() => {
      this.setPopupVisible(visible, point);
      this.clearDelayTimer();
    }, delay);
  } else {
    this.setPopupVisible(visible, event);
  }
},

3.3 其他函数

  1. contains函数:判断一个节点是否是另一个节点的子节点
export default function contains(root: HTMLElement | null | undefined, n?: HTMLElement) {
  if (!root) {
    return false;
  }

  return root.contains(n);
}
  1. onClick函数
onClick(event) {
  this.fireEvents('onClick', event);
  /**
  * 聚焦会触发click事件,如果这两个事件时间不超过20ms,则不触发click事件
  * 因为onFocus事件已经把visible修改了,不需要多次修改
  */
  if (this.focusTime) {
    let preTime;
    if (this.preClickTime && this.preTouchTime) {
      preTime = Math.min(this.preClickTime, this.preTouchTime);
    } else if (this.preClickTime) {
      preTime = this.preClickTime;
    } else if (this.preTouchTime) {
      preTime = this.preTouchTime;
    }
    if (Math.abs(preTime - this.focusTime) < 20) {
      return;
    }
    this.focusTime = 0;
  }
  this.preClickTime = 0;
  this.preTouchTime = 0;
  // Only prevent default when all the action is click.
  // https://github.com/ant-design/ant-design/issues/17043
  // https://github.com/ant-design/ant-design/issues/17291
  if (
    this.isClickToShow() &&
    (this.isClickToHide() || this.isBlurToHide()) &&
    event &&
    event.preventDefault
  ) {
    event.preventDefault();
  }
  if (event && event.domEvent) {
    event.domEvent.preventDefault();
  }
  const nextVisible = !this.$data.sPopupVisible;
  if ((this.isClickToHide() && !nextVisible) || (nextVisible && this.isClickToShow())) {
    this.setPopupVisible(!this.$data.sPopupVisible, event);
  }
}

/**
* focus的时候,会更新focusTime
*/
onFocus(e) {
  this.fireEvents('onFocus', e);
  // incase focusin and focusout
  this.clearDelayTimer();
  if (this.isFocusToShow()) {
    this.focusTime = Date.now();
    this.delaySetPopupVisible(true, this.$props.focusDelay);
  }
},

4 Portal组件的实现

源码地址:https://github.com/vueComponent/ant-design-vue/blob/main/components/_util/Portal.tsx

去掉多余的判断,剩下的逻辑就是:挂载时生成容器,卸载时删除容器,更新时执行传入的方法。

setup(props, { slots }) {
  // getContainer 不会改变,不用响应式
  let container: HTMLElement;
  onBeforeMount(() => {
    container = props.getContainer();
  });

  onUpdated(() => {
    nextTick(() => {
      props.didUpdate?.(props);
    });
  });
  onBeforeUnmount(() => {
    if (container && container.parentNode) {
      container.parentNode.removeChild(container);
    }
  });
  return () => {
    return container ? <Teleport to={container} v-slots={slots}></Teleport> : null;
  };
},

5 总结

本篇对Trigger组件和Portal组件的核心代码进行分析,剩下的都是事件处理函数,可以自行阅读。需要注意的是visible相关的处理都进行了延时,防止错误。

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

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

相关文章

构建响应式 Web 应用:Vue.js 基础指南

构建响应式 Web 应用&#xff1a;Vue.js 基础指南 一 . Vue 的介绍1.1 介绍1.2 好处1.3 特点 二 . Vue 的快速入门2.1 案例 1 : 快速搭建 Vue 的运行环境 , 在 div 视图中获取 Vue 中的数据2.2 案例 2 : 点击按钮执行 vue 中的函数输出 vue 中 data 的数据2.3 小结 三 . Vue 常…

Leetcode3282. 到达数组末尾的最大得分

Every day a Leetcode 题目来源&#xff1a;3282. 到达数组末尾的最大得分 解法1&#xff1a;动态规划 代码&#xff1a; class Solution { public:long long findMaximumScore(vector<int>& nums) {if (nums.size() < 1) return 0LL;int n nums.size();vect…

JavaScript高级——循环遍历加监听

本文分享到这里&#xff0c;欢迎大家评论区相互讨论学习&#xff0c;下一篇继续分享JavaScript高级学习中的闭包的内容。

linux进程间通信——学习与应用命名管道, 日志程序的使用与实现

前言&#xff1a;本节主要讲解linux进程间通信里面的命名管道&#xff0c; 命名管道和我们学过的匿名管道是类似的。 博主将会带着友友们先看一下原理&#xff0c; 然后就会着手使用以下命名管道是怎么使用的。 最后我们还会试着引入日志系统&#xff0c; 我们从本节开始就会引…

npm 安装 与 切换 淘宝镜像

一、镜像源 npm默认镜像源是国外的&#xff0c;安装依赖速度较慢&#xff0c;使用国内的镜像源速度会快一些。 1、设置淘宝镜像源&#xff1a; #最新地址 淘宝 NPM 镜像站喊你切换新域名啦! npm config set registry https://registry.npm.taobao.org&#xff08;弃用了&…

网站采用H5+CSS3开发的优势和劣势

在现代网站开发中&#xff0c;HTML5和CSS3的结合使用已经成为一种趋势。以下是其优势和劣势的介绍&#xff1a; 优势 增强的多媒体支持&#xff1a;HTML5引入了新的标签&#xff0c;使开发者能够轻松嵌入音频、视频和图形&#xff0c;无需依赖第三方插件如Flash。这大大简化了…

【AI大模型】ChatGPT模型原理介绍(下)

目录 &#x1f354; GPT-3介绍 1.1 GPT-3模型架构 1.2 GPT-3训练核心思想 1.3 GPT-3数据集 1.4 GPT-3模型的特点 1.5 GPT-3模型总结 &#x1f354; ChatGPT介绍 2.1 ChatGPT原理 2.2 什么是强化学习 2.3 ChatGPT强化学习步骤 2.4 监督调优模型 2.5 训练奖励模型 2.…

基于单片机的风机故障检测装置的设计与实现(论文+源码)

1 系统总体设计方案 通过对风机故障检测装置的设计与实现的需求、可行性进行分析&#xff0c;本设计风机故障检测装置的设计与实现的系统总体架构设计如图2-1所示&#xff0c;系统风机故障检测装置采用STM32F103单片机作为控制器&#xff0c;并通过DS18B20温度传感器、ACS712电…

macOS使用brew安装并配置python环境

1.确认已安装brew环境,如没有安装,参考: macOS系统Homebrew工具安装及使用-CSDN博客 2.安装python python安装成功 3.添加pip路径到/etc/paths 4.查看python与pip默认安装版本

【leetcode】树形结构习题

二叉树的前序遍历 返回结果&#xff1a;[‘1’, ‘2’, ‘4’, ‘5’, ‘3’, ‘6’, ‘7’] 144.二叉树的前序遍历 - 迭代算法 给你二叉树的根节点 root &#xff0c;返回它节点值的 前序 遍历。 示例 1&#xff1a; 输入&#xff1a;root [1,null,2,3] 输出&#xff1a;[1,…

git 更换远程地址的方法

需要将正在开发的代码远程地址改成新的地址&#xff0c;通过查询发现有三个方法可以实现&#xff0c;特此记录。具体方法如下&#xff1a; &#xff08;1&#xff09;通过命令直接修改远程仓库地址 git remote 查看所有远程仓库git remote xxx 查看指定远程仓库地址git remote…

外卖会员卡是不是一个骗局?

大家好&#xff0c;我是鲸天科技千千&#xff0c;大家都知道我是做小程序开发的&#xff0c;平时会给大家分享一些互联网相关的创业项目&#xff0c;感兴趣的可以跟我关注一下。 首先就是要搭建一个自己的外卖会员卡系统小程序&#xff0c;我们自己的工作就是把这个小程序推广…

JDBC注册驱动及获取连接

文章目录 1. JDBC注册驱动1.1 导入驱动 Jar 包1.2 注册驱动1.2.1 API介绍1.2.2 使用步骤1.2.3 案例代码 2. 获取连接2.1 API介绍2.2 参数说明2.3 注意事项2.4 使用步骤3.5 案例代码 1. JDBC注册驱动 Connection表示Java程序与数据库之间的连接&#xff0c;只有拿到Connection才…

TCP/IP网络模型分层

应用层 应用层是最上层的&#xff0c;也就是我们能直接接触到的就是应用层(Application Layer),手机和电脑上的应用软件都是在应用层实现。当两个不同设备的应用需要通信的时候&#xff0c;应用就会把数据传输给下一层&#xff0c;也就是传输层 所以&#xff0c;应用层只需要…

PMP--一模--解题--91-100

文章目录 13.干系人管理91、 [单选] 在项目执行期间&#xff0c;一名外部干系人反对一项重大范围变更。除非重新评估干系人的决定&#xff0c;否则项目进展将受到影响。项目经理下一步该怎么做&#xff1f; 5.范围管理92、 [单选] 一客户给你一复杂项目的采购工作说明书&#x…

「数组」堆排序 / 大根堆优化(C++)

目录 概述 核心概念&#xff1a;堆 堆结构 数组存堆 思路 算法过程 up() down() Code 优化方案 大根堆优化 Code(pro) 复杂度 总结 概述 在「数组」快速排序 / 随机值优化|小区间插入优化&#xff08;C&#xff09;中&#xff0c;我们介绍了三种基本排序中的冒泡…

数学学习记录

9月14日 1.映射&#xff1a; 2.函数: 9月15日 3.反函数&#xff1a; 4.收敛数列的性质 5.反三角函数&#xff1a; 9月16日 6.函数的极限&#xff1a; 7.无穷小和无穷大 极限运算法则&#xff1a;

MySQL_简介及安装、配置、卸载(超详细)

课 程 推 荐我 的 个 人 主 页&#xff1a;&#x1f449;&#x1f449; 失心疯的个人主页 &#x1f448;&#x1f448;入 门 教 程 推 荐 &#xff1a;&#x1f449;&#x1f449; Python零基础入门教程合集 &#x1f448;&#x1f448;虚 拟 环 境 搭 建 &#xff1a;&#x1…

小麦病害检测数据集【‘细菌叶斑病‘, ‘褐斑病‘, ‘叶瘤病‘】

小麦病害检测数据集】nc3 标签names:[Bacteria Leaf Blight,Brown Spot, Leaf smut] 名称&#xff1a;【细菌叶斑病, 褐斑病, 叶瘤病】共6715张&#xff0c;8:1:1比例划分&#xff0c;&#xff08;train;5372张&#xff0c;val&#xff1a;671张&#xff0c;test&#xff1a;67…

【AI视频】复刻抖音爆款AI数字人作品初体验

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AI视频 | AI数字人 文章目录 &#x1f4af;前言&#x1f4af;抖音上的爆火AI数字人视频&#x1f4af;注册HeyGen账号&#x1f4af;复刻抖音爆款AI数字人&#x1f4af;最终生成效果&#x1f4af;小结 对比原视频效果&#xff1a;…