vc-align源码分析 -- ant-design-vue系列

news2024/12/24 9:14:47

vc-align源码分析

源码地址:https://github.com/vueComponent/ant-design-vue/tree/main/components/vc-align

1 基础代码

1.1 名词约定

需要对齐的节点叫source,对齐的目标叫target

1.2 props

提供了两个参数:

  1. align:对齐的配置
  2. target:一个函数,用于获取对齐的目标dom

1.3 主要逻辑

  1. 增加了一个dom,用来挂载source节点,同时拿到它的引用。
  2. 提供了一个方法align,在组件初始化/定位方式改变/对齐目标改变的时候,重新执行对齐方法。

代码如下:

import { defineComponent, ref, onMounted, watch, PropType } from 'vue';
import { alignElement } from 'dom-align';
import { AlignType, TargetType } from './interface';

export default defineComponent({
	name: 'Align',
	props: {
		align: {
			type: Object as PropType<AlignType>,
			required: true
		},
		target: {
			type: [Object, Function] as PropType<TargetType>,
			required: true
		}
	},
	setup(props, { slots }) {
		const nodeRef = ref<HTMLElement | null>(null);

    /**
    * 用来对齐的方法
    */
		const align = () => {
			if (!nodeRef.value) return;

			const { align: latestAlign, target: latestTarget } = props;

			let result: any;
			let targetElement: HTMLElement | null = null;

			if (typeof latestTarget === 'function') {
				targetElement = latestTarget();
			}

			if (targetElement && targetElement.nodeType === Node.ELEMENT_NODE) {
        /**
        * 调用对齐的库方法
        */
				result = alignElement(nodeRef.value, targetElement, latestAlign);
			}
		};

		onMounted(() => {
			align();
		});

    /**
    * 监控对齐方式和target的改变,重新执行对齐
    */
		watch(
			() => [props.align, props.target],
			() => {
				align();
			},
			{ immediate: true, deep: true, flush: 'post' }
		);

		return () => {
			const child = slots.default?.();
			if (child) {
				return <div ref={nodeRef}>{child}</div>;
			}
			return null;
		};
	}
});

1.4 补充:dom-align 库

官方地址:https://yiminghe.me/dom-align/

1.4.1 基础用法
import domAlign from 'dom-align';

// use domAlign
// sourceNode's initial style should be position:absolute;left:-9999px;top:-9999px;

const alignConfig = {
  points: ['tl', 'tr'],        
  offset: [10, 20],           
  targetOffset: ['30%','40%'], 
  overflow: { adjustX: true, adjustY: true },
};

domAlign(sourceNode, targetNode, alignConfig);
1.4.2 alignConfig对象的详细配置
NameTypeDescription
pointsString[2]source元素和targer元素的对齐方式,比如 [‘tr’, ‘cc’],意思是source元素的右上角和target元素的中心对齐。点的取值可以是t, b, c, l, r。
offsetNumber[2]source元素的偏移量,offset[0] 是x轴,offset[1]是y轴。如果数组中包含了百分比,这个也是相对应source区域来说的。
targetOffsetNumber[2]和上面一致,只不过都是针对target元素来说的。
overflowObject: { adjustX: boolean, adjustY: boolean, alwaysByViewport:boolean }如果adjustX是true,那么如果source元素在x轴方向不可见,会自动调整位置。比如指定source元素在target右边,但是右边区域不足以放得下source,则会自动修改到做左边展示。adjustY同理。如果alwaysByViewport是true,那么当source不在视口中时,会自动调整。
useCssRightBoolean是否使用css的right属性代替left属性去定位。
useCssBottomBoolean是否使用css的bottom属性代替top属性去定位。
useCssTransformBoolean是否使用css的transform属性代替 left/top/right/bottom来定位。

2 源码解析

2.1 可以优化的点

  1. 我们给source增加了一个div,用来获取引用,这个dom节点是不必要,可以去掉。
  2. 只监控了 对齐方式/target引用 的变化,没有监控sourcetarget大小的变化,需要在这些属性变化时,重新对齐。
  3. 需要监控窗口大小的变化,重新对齐。

2.2 实现

2.2.1 监控window变化

这个有resize事件,直接组册即可。

组件需要接受一个props,表示是否需要监控window变化。

export const alignProps = {
  monitorWindowResize: Boolean,
};

代码如下,flush: post是为了保证页面已经渲染结束,可以拿到dom引用。

/**
* 用来记录监控事件的id
*/
const winResizeRef = ref<{ remove: Function }>(null);

watch(
  () => props.monitorWindowResize,
  (monitorWindowResize) => {
    if (monitorWindowResize) {
     /**
	 * 需要监控window大小变化,但是以前没有注册过监控事件
	 */
      if (!winResizeRef.value) {
        winResizeRef.value = window.addEventListener('resize', forceAlign);
      }
    } else if (winResizeRef.value) {
     /**
	 * 如果不需要监控,但是已经监控过了,那就取消监控
	 */
      winResizeRef.value.remove();
      winResizeRef.value = null;
    }
  },
  { immediate: true, flush: 'post' }
);
2.2.2 监控source和target的变化
  • 需要手写一个监控的函数

这里需要一个新的接口:ResizeObserver https://developer.mozilla.org/zh-CN/docs/Web/API/ResizeObserver

使用这个接口,可以监听一个DOM节点的变化,这种变化包括但不仅限于:

  1. 某个节点的出现和隐藏
  2. 某个节点的大小变化

我们用它来观察指定的元素,如果元素变化,执行指定的回调。

export function monitorResize(element: HTMLElement, callback: Function) {
  /**
   * 1 初始化一个观察器
   * onResize 是元素变化后的回调
   */
  const resizeObserver = new ResizeObserver(onResize);

  /**
   * 2 观察指定的DOM元素 element
   */
  if (element) {
    resizeObserver.observe(element);
  }
  
  // ......

  /**
   * 3 返回一个函数,用于取消观察
   */
  return () => {
    resizeObserver.disconnect();
  };
}

每次都用当前大小和上次的大小比较,如果不一致,执行callback回调。

export function monitorResize(element: HTMLElement, callback: Function) {
  
  // ......
  
  let prevWidth: number = null;
  let prevHeight: number = null;

  /**
   * 4 当元素大小变化时,调用用户传入的 callback 方法
   */
  function onResize([{ target }]: ResizeObserverEntry[]) {
    if (!document.documentElement.contains(target)) return;
    const { width, height } = target.getBoundingClientRect();
    const fixedWidth = Math.floor(width);
    const fixedHeight = Math.floor(height);

    if (prevWidth !== fixedWidth || prevHeight !== fixedHeight) {
      // https://webkit.org/blog/9997/resizeobserver-in-webkit/
      Promise.resolve().then(() => {
        callback({ width: fixedWidth, height: fixedHeight });
      });
    }

    prevWidth = fixedWidth;
    prevHeight = fixedHeight;
  }
}
  • 在页面挂载的时候,注册监控事件;在页面属性更新的时候(比如source或者target变化时),需要清除旧的事件,注册新的事件
onMounted(() => {
  nextTick(() => {
    /**
    * goAlign 用来维护监控事件,同时执行对齐方法
    * 实现在下面。
    */
    goAlign();
  });
});

 onUpdated(() => {
   nextTick(() => {
     goAlign();
   });
 });

因为要清除旧的事件,所以需要需要保存 注册方法返回的 resizeObserver.disconnect(),方便执行清除的时候调用;同时记录下来当前引用的dom节点,来判断是否需要注册新的监听事件。

interface MonitorRef {
  element?: HTMLElement; // 当前`dom`节点的引用
  cancel: () => void;    // 监控事件的取消方法
}

// Listen for target updated
const targetResizeMonitor = ref<MonitorRef>({
  cancel: () => {},
});
// Listen for source updated
const sourceResizeMonitor = ref<MonitorRef>({
  cancel: () => {},
});

goAlign()的实现

const goAlign = () => {
  const target = props.target;
  const element = getElement(target);
  const point = getPoint(target);

  /**
  * onMounted 的时候,必定执行;onUpdated 的时候,只有source的引用变了才会执行
  * 清除旧的监听事件,注册新的
  */ 
  if (nodeRef.value !== sourceResizeMonitor.value.element) {
    sourceResizeMonitor.value.cancel();
    sourceResizeMonitor.value.element = nodeRef.value;
    sourceResizeMonitor.value.cancel = monitorResize(nodeRef.value, forceAlign);
  }

  /**
  * 如果缓存的target和当前的target不一致,或者对齐方式不一致,就执行对齐方法
  * 同时如果target变了,清除旧的监听事件,注册新的
  */
  if (
    cacheRef.value.element !== element ||
    !isSamePoint(cacheRef.value.point, point) ||
    !isEqual(cacheRef.value.align, props.align)
  ) {
    forceAlign();

    // Add resize observer
    if (resizeMonitor.value.element !== element) {
      resizeMonitor.value.cancel();
      resizeMonitor.value.element = element;
      resizeMonitor.value.cancel = monitorResize(element, forceAlign);
    }
  }
};
2.2.3 重写对齐的方法

因为我们监控了元素大小的变化,触发频率很高,也就是说对齐方法执行的频率也会非常高。

所以需要一个方法,这个方法需要实现类似防抖的功能。源码是使用useBuffer实现的,我们先看一下这个方法。

export const alignProps = {
  monitorBufferTime: Number,
};

/**
* 返回了一个强制执行的方法和一个取消执行的方法
*/
const [forceAlign, cancelForceAlign] = useBuffer(
  () => {
  	// ...... 对齐的方法
  },
  computed(() => props.monitorBufferTime),
);
  • useBuffer的实现
/**
 * 这个函数设计用于控制一个基于时间缓冲的触发逻辑,确保在一定时间间隔内(由buffer参数指定)
 * 即使多次尝试触发,也只有一次实际执行callback的机会,除非通过强制执行(force参数为true)来绕过这个缓冲逻辑。
 *
 * 提供了执行的方法和取消执行的方法
 */
export default (callback: () => boolean, buffer: ComputedRef<number>) => {
  let called = false;
  let timeout = null;

  function cancelTrigger() {
    clearTimeout(timeout);
  }

  function trigger(force?: boolean) {
   // ......
  }

  return [
    trigger,
    () => {
      called = false;
      cancelTrigger();
    },
  ];
};

执行方法trigger的实现如下:

  1. 不在回调过程中:直接设置定时
  2. 如果是强制触发:取消旧的定时,设置新的定时
  3. 在回调过程中:取消旧的定时,设置新的定时
function trigger(force?: boolean) {
  // 如果不在回调过程中 || 强制触发,则
  if (!called || force === true) {
    // 执行一遍callback,如果返回了false,就不需要延迟
    if (callback() === false) {
      // Not delay since callback cancelled self
      return;
    }

    called = true;
    // 取消上次的定时,重新定时
    cancelTrigger();
    timeout = setTimeout(() => {
      called = false;
    }, buffer.value);
  } else {
    // 在回调过程中:取消上次的定时,重新定时
    cancelTrigger();
    timeout = setTimeout(() => {
      called = false;
      trigger();
    }, buffer.value);
  }
}

buffer时间结束后,会执行对齐函数。

  • 对齐的方法
const cacheRef = ref<{ element?: HTMLElement; point?: TargetPoint; align?: AlignType }>({});
const nodeRef = ref();
const [forceAlign, cancelForceAlign] = useBuffer(
  () => {
    const {
      disabled: latestDisabled,
      target: latestTarget,
      align: latestAlign,
      onAlign: latestOnAlign,
    } = props;
    if (!latestDisabled && latestTarget && nodeRef.value) {
      const source = nodeRef.value;

      /**
      * 获取了目标元素或者对齐点。
      */
      let result: AlignResult;
      const element = getElement(latestTarget);
      const point = getPoint(latestTarget);

      /**
      * 缓存目标元素的信息和对齐方式
      */
      cacheRef.value.element = element;
      cacheRef.value.point = point;
      cacheRef.value.align = latestAlign;

      // 🚁 IE浏览器在元素对齐后会失去焦点,所以需要在对齐后重新聚焦
      /**
      * 记录了当前文档中的活动元素(activeElement),以便在对齐操作后恢复焦点
      */
      const { activeElement } = document;
      // 只有元素可见才需要对齐
      if (element && isVisible(element)) {
        result = alignElement(source, element, latestAlign);
      } else if (point) {
        result = alignPoint(source, point, latestAlign);
      }
      restoreFocus(activeElement, source);

      /**
      * 如果调用者需要在对齐后做一些事情,就执行props传进来的回调方法
      */
      if (latestOnAlign && result) {
        latestOnAlign(source, result);
      }

      return true;
    }

    return false;
  },
  computed(() => props.monitorBufferTime),
);

target节点为啥要缓存下来?

onUpdated中,调用了goAlign()props中的target是一个函数,可能对于同一个target节点,引用发生变化(调用者每次都给target一个新的函数),引起不必要的重新对齐操作。

2.2.4 给插槽元素增加ref引用

这里的实现比较简单,先看代码。主要逻辑就是cloneElement,在复制的时候重写了他的属性。

return () => {
  const child = slots?.default();
  if (child) {
    return cloneElement(child[0], { ref: nodeRef }, true, true);
  }
  return null;
};

看一下这个函数的实现。调用了vuecloneVNode方法,把{ ref: nodeRef }加入到虚拟节点的属性中。

import { cloneVNode } from 'vue';

export function cloneElement<T, U>(
  vnode: VNode<T, U> | VNode<T, U>[],
  nodeProps: Record<string, any> &
    Omit<VNodeProps, 'ref'> & { ref?: VNodeProps['ref'] | RefObject } = {},
  override = true,
  mergeRef = false,
): VNode<T, U> {
  let ele = vnode;
  if (Array.isArray(vnode)) {
    ele = filterEmpty(vnode)[0];
  }
  if (!ele) {
    return null;
  }
  const node = cloneVNode(ele as VNode<T, U>, nodeProps as any, mergeRef);

  // cloneVNode内部是合并属性,这里改成覆盖属性
  node.props = (override ? { ...node.props, ...nodeProps } : node.props) as any;
  return node;
}

3 效果演示

3.1 resize变化

当窗口大小变化时,对自适应对齐方式。以纵向为例。

在这里插入图片描述

3.2 source 和target大小变化

分别修改二者大小,都可以重新触发对齐操作。

在这里插入图片描述

3.3 插槽引用

source节点没有增加一个div包裹,同时也拿到了它的引用进行定位。

在这里插入图片描述

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

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

相关文章

成为优秀程序员-代码篇

1. 序言 刚毕业参加工作时候&#xff0c;公司正在快速扩张&#xff0c;我入职的时候组内刚刚招了一大波人&#xff0c;当时leader提出集体cr代码来拉齐团队内的编码规范&#xff0c;每当有对于相对重要改动大的项目就会集体cr代码&#xff0c;老板想法初衷是好的&#xff0c;但…

esp32 gpio 初始化不同类型的管脚,产生脉冲,发生中断

硬件&#xff1a;D4与D18 连接&#xff0c;二极管接D15与3.3v脚 图片 二极管同期性点亮&#xff0c;间隔1秒 参考esp32官网程序&#xff0c;从此程序可以看出&#xff0c;中断程序没有处理任何数据&#xff0c;只是把中断发生的事件存入队列。而用另一新线程来处理中断事务。…

GNSS CTS GNSS Start and Location Flow of Android15

目录 1. 本文概述2.CTS 测试3.Gnss Flow3.1 Gnss Start Flow3.2 Gnss Location Output Flow 1. 本文概述 本来是为了做Android 14 Gnss CTS 的相关环境的搭建和测试&#xff0c;然后在测试中遇到了一些问题&#xff0c;去寻找CTS源码(/cts/tests/tests/location/src/android/l…

Vue3-05_组件高级

背景 对组件的进一步了解,如组件之间通信等知识点&#xff0c;根据教程实现购物车功能&#xff0c;并修复原本的bug. watch 侦听器 用途 watch 侦听器允许开发者监视数据的变化&#xff0c;从而针对数据的变化做特定的操作。例如&#xff0c;监视用户名的变化并发起请求&am…

大模型AI一体机对行业的帮助

大模型AI一体机&#xff0c;如AntSKPro AI离线知识库一体机&#xff0c;是专门为企业和机构设计的集成系统&#xff0c;旨在提供高效的人工智能服务。这类一体机通常包含预训练的大型机器学习模型&#xff0c;以及必要的硬件和软件资源&#xff0c;以支持复杂的数据处理和分析任…

maven 编译构建可以执行的jar包

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storm…

网易云音乐歌单下载器

最近要帮小朋友下载一些小学的诗词mp3&#xff0c;找了各种工具&#xff0c;还是这个好使 yun-playlist-downloader: 网易云音乐歌单下载器 特性 支持歌单 / 专辑 / 电台音质选择下载超时 / 重试再次下载默认跳过已下载部分, 使用 content-length 匹配自定义文件名下载进度显…

【代码随想录训练营第42期 Day53打卡 - 图论Part4 - 卡码网 110. 字符串接龙 105. 有向图的完全可达性

目录 一、个人感受 二、题目与题解 题目一&#xff1a;卡码网 110. 字符串接龙 题目链接 题解&#xff1a;BFS哈希 题目二&#xff1a;卡码网 105. 有向图的完全可达性 题目链接 题解&#xff1a;DFS 三、小结 一、个人感受 对于两大基本搜索&#xff1a; 深度优先搜…

JDBC:连接数据库

文章目录 报错 报错 Exception in thread “main” java.sql.SQLException: Can not issue SELECT via executeUpdate(). 最后这里输出的还是地址&#xff0c;就是要重写toString()方法&#xff0c;但是我现在还不知道怎么写 修改完的代码&#xff0c;但是数据库显示&#…

WebGL系列教程二(环境搭建及初始化Shader)

目录 1 前言2 新建html页面3 着色器介绍3.1 顶点着色器、片元着色器与光栅化的概念3.2 声明顶点着色器3.3 声明片元着色器 4 坐标系(右手系)介绍5 着色器初始化5.1 给一个画布canvas5.2 获取WebGL对象5.3 创建着色器对象5.4 获取着色器对象的源5.5 绑定着色器的源5.6 编译着色器…

MiniGPT-3D, 首个高效的3D点云大语言模型,仅需一张RTX3090显卡,训练一天时间,已开源

项目主页&#xff1a;https://tangyuan96.github.io/minigpt_3d_project_page/ 代码&#xff1a;https://github.com/TangYuan96/MiniGPT-3D 论文&#xff1a;https://arxiv.org/pdf/2405.01413 MiniGPT-3D在多个任务上取得了SoTA&#xff0c;被ACM MM2024接收&#xff0c;只拥…

佰朔资本:9月首选行业为汽车、电子、医药生物等

5—8月商场接连调整&#xff0c;9月开端进入成绩空窗期&#xff0c;流动性和政策改动从头成为商场中心驱动力&#xff0c;风格切换先行&#xff0c;对当时的商场能够豁达一些。价值和生长风格切换的拐点开始闪现&#xff0c;生长相对价值的成绩优势开端走扩&#xff0c;美联储降…

Axure中继器介绍

中继器我们一般在处理重复性比较高的任务时&#xff0c;能让我们达到事半功倍的效果&#xff0c;中继器在整个axure中属于复杂程度比较高的功能&#xff0c;我们今天大致讲一下常用的方法即可。 一、声明一个中继器 默认展示为三行。 点击样式&#xff0c;这里我们可以添加删…

【原创】java+springboot+mysql校园二手商品交易网设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

RestTemplateRibbonOpenFeign

网络模型 OSI七层模型 RestTemplate Ribbon 在微服务中的ribbon 实现负载均衡服务间调用的三种方式 ribbon其他负载均衡策略 OpenFeign 实战

【Shiro】Shiro 的学习教程(五)之 SpringBoot 集成 Shiro + JWT

与 Spring 集成&#xff1a;与 Spring 集成 与 SpringBoot 集成&#xff1a;与 SpringBoot 集成 1、SpringBoot Shiro Jwt ①&#xff1a;引入 pom.xml&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-…

使用kubeadm手动安装K8s

本次教程安装主要基于Ubuntu 22.04&#xff0c; 使用AWS EC2服务器来部署。当然&#xff0c;AWS也有自己的AWS K8s服务&#xff0c;不过需要花费小钱钱。虽然也不是说不行&#xff0c;但手动安装下也能熟悉K8s。 1. 安装Docker 卸载旧版本&#xff1a; sudo apt-get re…

数据结构与算法 第12天(排序)

一、排序方法分类 按照数据存储介质&#xff1a; 内部排序&#xff1a;数据量不大、数据在内存&#xff0c;无需内外存交换数据 外部排序&#xff1a;数据量较大、数据在外存(文件排序) 将数据分批调入内存排序&#xff0c;结果放到外存 按照比较器个数&#xff1a; 串行…

微带结环行器仿真分析+HFSS工程文件

微带结环行器仿真分析HFSS工程文件 工程下载&#xff1a;微带结环行器仿真分析HFSS工程文件 我使用HFSS版本的是HFSS 2024 R2 参考书籍《微波铁氧体器件HFSS设计原理》和视频微带结环行器HFSS仿真 1、环形器简介 环行器是一个有单向传输特性的三端口器件&#xff0c;它表明…

大数据之Flink(六)

17、Flink CEP 17.1、概念 17.1.1、CEP CEP是“复杂事件处理&#xff08;Complex Event Processing&#xff09;”的缩写&#xff1b;而 Flink CEP&#xff0c;就是 Flink 实现的一个用于复杂事件处理的库&#xff08;library&#xff09;。 总结起来&#xff0c;复杂事件处…