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

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相关的处理都进行了延时,防止错误。

  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Ant Design Vue 是一个基于 Ant DesignVue 的组件库,它提供了一套高质量的 Vue 组件实现,帮助开发者快速构建具有统一设计风格的用户界面。在使用 Ant Design Vue 进行表单处理时,如果你想要在保存时提取表单数据,通常需要进行以下几个步骤: 1. 使用 `a-form` 组件创建表单,并为其添加 `model` 属性以绑定表单数据。 2. 为每个需要获取数据的表单字段添加相应的 `a-form-item` 容器,并在内部放置相应的输入组件,如 `a-input`、`a-select` 等。 3. 通过 `rules` 属性为表单字段设置验证规则,确保在提交之前表单数据的有效性。 4. 在提交按钮的点击事件中,使用 `this.$refs.form.validate()` 方法验证表单字段是否符合规则。 5. 如果验证通过,可以使用 `this.$refs.form.getFieldsValue()` 或 `this.$refs.form.getFields()` 方法提取表单的数据。 以下是一个简单的示例代码: ```html <template> <a-form ref="form" :model="form" :rules="rules"> <a-form-item label="用户名" name="username" rules={[{ required: true, message: '请输入用户名!' }]}> <a-input v-model:value="form.username"></a-input> </a-form-item> <!-- 其他表单项... --> <a-form-item> <a-button type="primary" @click="submitForm">提交</a-button> </a-form-item> </a-form> </template> <script> export default { data() { return { form: { username: '', // 其他字段... }, rules: { username: [ { required: true, message: '用户名必填', trigger: 'blur' }, // 其他规则... ], // 其他字段规则... } }; }, methods: { submitForm() { this.$refs.form.validate((valid) => { if (valid) { const formData = this.$refs.form.getFieldsValue(); // 或者使用 this.$refs.form.getFields() 获取更详细的信息 console.log('表单数据:', formData); // 这里可以进行保存操作,比如发送到后端服务器 } else { console.error('表单验证失败'); return false; } }); } } }; </script> ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值