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

news2024/12/27 14:02:03

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/2122996.html

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

相关文章

迟滞比较器/施密特触发器

功能 从下面原理图像看来&#xff0c;只有在达到上下阈值才会出现输出电平的转换&#xff0c;这样防止信号的杂波跳变。而且每次的阈值是随着输出而变化的&#xff0c;当输出高时&#xff0c;阈值如下图中&#xff0c;V_PV_N V_R*( RF/(R1RF) )VH*( R1/(R1RF) );当输出低时&a…

QT核心机制

目录 学习内容&#xff1a; 1. 对话框 1.1 消息对话框&#xff08;QMessageBox&#xff09; 1.2 消息对话框实例 1.3 颜色对话框&#xff08;QColorDialog&#xff09;、字体对话框&#xff08;QFontDialog&#xff09;、文件对话框&#xff08;QFileDialog&#xff09; …

Python面试常见问题及详细解答:从基础到高级概念全覆盖

创作不易&#xff0c;您的打赏、关注、点赞、收藏和转发是我坚持下去的动力&#xff01; 以下是Python面试中常见的一些问题及其详细答案的整理&#xff1a; 1. Python的可变与不可变对象 问题: 什么是可变对象和不可变对象&#xff1f;举例说明。答案: 可变对象: 可以在原地…

实现卷积层的前向传播(Pythom版)

在TensorFlow框架中&#xff0c;实现卷积层&#xff08;2维&#xff09;的代码是 tf.keras.layers.Conv2D()。它主要接收如下几个参数&#xff0c; filters&#xff1a;卷积核的个数&#xff0c;也就是卷积层输出的通道数&#xff08;沿axis-1的维度&#xff09; kernel_size&a…

AI秒画损失函数曲线图(Loss Function Curve)

在深度学习模型训练中&#xff0c;Loss曲线图是衡量模型性能的一个重要指标。通过绘制Loss曲线&#xff0c;能够清楚地观察到模型在训练过程中的收敛情况&#xff0c;从而帮助我们判断模型是否出现过拟合或欠拟合。本文将介绍如何通过简单几步&#xff0c;快速绘制出训练的Loss…

iphone16-iphone16pro原壁纸分享

iphone16-iphone16pro原壁纸分享 苹果公司在2024年9月10日的秋季新品发布会上正式推出了iPhone 16系列智能手机。以下是iPhone 16系列的主要特点和更新&#xff1a; 全新A18芯片&#xff1a;iPhone 16系列搭载了苹果最新的A18芯片&#xff0c;这款芯片专为苹果智能&#xff08;…

【WebGIS实例】(16)GeoServer 自定义样式 - 渲染矢量数据

1. 前言 本篇博客将会分享一系列的 GeoServer 样式&#xff0c;通过这些样式预先在服务端完成数据渲染&#xff0c;让前端应用更便捷的加载数据服务。 2. 面矢量 示例数据&#xff1a; {type: FeatureCollection,features: [{type: Feature,properties: {分类字段: 字段一…

PPT复制图表时颜色发生变化怎么办?

有时可能想复制其他PPT的图表到另一个PPT里&#xff0c;复制过来发现颜色发生了变化&#xff0c;这与我们PPT中的主题色颜色不同有关&#xff0c;所以就导致了图表的变色。 以上两张图片就是发生了变色的情况&#xff0c;一个是原来的颜色&#xff0c;一个是变化后的颜色。 解…

【鸿蒙】HarmonyOS NEXT星河入门到实战4-ArkTS界面布局深入

目录 一、布局元素组成 1.1 内边距-padding 1.2 外边距 margin 1.3 实战案例-QQ音乐-登录 1.4 边框 border 二、设置组件圆角 2.1 基本圆角设置 2.2 特殊形状的圆角设置 三、背景属性 3.1 背景图片-backgroundImage 3.2 背景图片位置-backgroundImagePosition 3.3 背…

什么是梯度? 梯度的作用 梯度的应用场景。

梯度 是一个非常重要的概念,它直接关系到模型的训练过程。以下是对梯度的详细解释: 梯度的基本概念 梯度 是一个向量,表示函数在某一点的导数或偏导数。在多维空间中,梯度指向的是函数值上升最快的方向。对于一个函数 f(x),在某一点 x 处的梯度记作 ∇f(x),它可以理解为在…

【Linux进程详解】进程地址空间

目录 1.直接写代码看现象 2.引入最基本的理解 3.细节问题-理解它 1.直接写代码看现象 #include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <unistd.h> int g_val 100;int main() {printf("fath…

常回家看看之house of kiwi

house of kiwi 前言&#xff1a;house_of_kiwi 一般是通过触发__malloc_assert来刷新IO流&#xff0c;最后可以劫持程序流或者通过和setcontext来打配合来进行栈迁移来得到flag。 我们看看触发的源码 #if IS_IN (libc) #ifndef NDEBUG # define __assert_fail(assertion, fi…

MFC之CString类及其成员函数用法详解

CString是 CStringT(属于MFC 和 ATL 之间共享的类) 的类模板的专用化&#xff0c;没有基类。在头文件atlstr.h中可以看到CString的定义&#xff1a; CString对象由可变长度的一队字符组成。CString是基于TCHAR数据类型的对象。如果在你的程序中定义了符号_UNICODE&#xff0c;则…

Leetcode 300. 最长递增子序列 记忆化搜索、贪心二分 C++实现

Leetcode 300. 最长递增子序列 问题&#xff1a;给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是…

猫头虎推荐:2024国内好用的PyPIP换源库

猫头虎推荐&#xff1a;2024国内好用的PyPIP换源库&#x1f525;&#x1f680; 在国内使用 Python 时&#xff0c;由于访问官方的 PyPI 速度较慢甚至无法连接&#xff0c;选择一个可靠的国内 PyPI 镜像源至关重要&#x1f4c8;。为了更高效地完成项目开发&#xff0c;今天猫头…

BC172 牛牛的排列数(c 语言)

1.我们先输入n m的数字&#xff0c;因为n!/(n-m)!的阶乘。即4&#xff01;4*3*2*1&#xff0c;2&#xff01;2*1&#xff0c;4&#xff01;/2&#xff01;12.或者4&#xff01;4*3*2&#xff01;。 #include<sdtio.h> int main() {int n 0;int m 0;long long a 1;whi…

Leetcode面试经典150题-55.跳跃游戏

解法都在代码里&#xff0c;不懂就留言或者私信 class Solution {public boolean canJump(int[] nums) {/**如果就一个位置&#xff0c;你本来就在这&#xff0c;肯定可以跳到*/if(nums.length 1) {return true;}/**这个题的解题思路是遍历数组&#xff0c;如果当前位置不在之…

Linux网络——从《计算机网络》到网络编程

文章目录 从《计算机网络》到网络编程从计算机到计算机网络解决问题网络与计算机系统计算机网络的传输流程IP地址与MAC地址 从《计算机网络》到网络编程 科班的同学大多学过计算机网络&#xff0c;而非科班的同学也多多少少听说过一些 计算机网络体系十分繁杂且精妙&#xff…

毕业论文任务书怎么写?超详细指导带你轻松搞定!

AIPaperGPT&#xff0c;论文写作神器~ https://www.aipapergpt.com/ 毕业论文任务书是毕业论文的“指路明灯”&#xff0c;是论文写作的路线规划。很多同学把毕业论文任务书当作形式化的文件草草了事&#xff0c;其实不然。任务书不仅是你整个论文写作的起点&#xff0c;也是确…

艺术体操与骑行的完美协奏:维乐Angel Rise+坐垫,激情与力量的展现!

在艺术体操的赛场上&#xff0c;每一次旋转、每一次跳跃&#xff0c;都凝聚着运动员的力量与技巧。这不仅是一场速度与激情的碰撞&#xff0c;更是一次力量与技巧的交融。正如在骑行的领域里&#xff0c;VELO Angel Rise坐垫以它独特的一体成型设计和技术&#xff0c;为骑行者们…