vc-align源码分析 -- ant-design-vue系列
vc-align源码分析
源码地址:https://github.com/vueComponent/ant-design-vue/tree/main/components/vc-align
1 基础代码
1.1 名词约定
需要对齐的节点叫source
,对齐的目标叫target
。
1.2 props
提供了两个参数:
align
:对齐的配置target
:一个函数,用于获取对齐的目标dom
1.3 主要逻辑
- 增加了一个
dom
,用来挂载source
节点,同时拿到它的引用。 - 提供了一个方法
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
对象的详细配置
Name | Type | Description |
---|---|---|
points | String[2] | source元素和targer元素的对齐方式,比如 [‘tr’, ‘cc’],意思是source元素的右上角和target元素的中心对齐。点的取值可以是t, b, c, l, r。 |
offset | Number[2] | source元素的偏移量,offset[0] 是x轴,offset[1]是y轴。如果数组中包含了百分比,这个也是相对应source区域来说的。 |
targetOffset | Number[2] | 和上面一致,只不过都是针对target元素来说的。 |
overflow | Object: { adjustX: boolean, adjustY: boolean, alwaysByViewport:boolean } | 如果adjustX是true,那么如果source元素在x轴方向不可见,会自动调整位置。比如指定source元素在target右边,但是右边区域不足以放得下source,则会自动修改到做左边展示。adjustY同理。如果alwaysByViewport是true,那么当source不在视口中时,会自动调整。 |
useCssRight | Boolean | 是否使用css的right属性代替left属性去定位。 |
useCssBottom | Boolean | 是否使用css的bottom属性代替top属性去定位。 |
useCssTransform | Boolean | 是否使用css的transform属性代替 left/top/right/bottom来定位。 |
2 源码解析
2.1 可以优化的点
- 我们给
source
增加了一个div
,用来获取引用,这个dom
节点是不必要,可以去掉。 - 只监控了 对齐方式/
target
引用 的变化,没有监控source
和target
大小的变化,需要在这些属性变化时,重新对齐。 - 需要监控窗口大小的变化,重新对齐。
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节点的变化,这种变化包括但不仅限于:
- 某个节点的出现和隐藏
- 某个节点的大小变化
我们用它来观察指定的元素,如果元素变化,执行指定的回调。
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 observerif (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
的实现如下:
- 不在回调过程中:直接设置定时
- 如果是强制触发:取消旧的定时,设置新的定时
- 在回调过程中:取消旧的定时,设置新的定时
function trigger(force?: boolean) {// 如果不在回调过程中 || 强制触发,则if (!called || force === true) {// 执行一遍callback,如果返回了false,就不需要延迟if (callback() === false) {// Not delay since callback cancelled selfreturn;}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;
};
看一下这个函数的实现。调用了vue
的cloneVNode
方法,把{ 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
包裹,同时也拿到了它的引用进行定位。
相关文章:

vc-align源码分析 -- ant-design-vue系列
vc-align源码分析 源码地址:https://github.com/vueComponent/ant-design-vue/tree/main/components/vc-align 1 基础代码 1.1 名词约定 需要对齐的节点叫source,对齐的目标叫target。 1.2 props 提供了两个参数: align:对…...

计算机网络(四) —— 简单Tcp网络程序
目录 一,服务器初始化 1.0 部分文件代码 1.1 关于Tcp协议 1.2 创建和绑定套接字 1.3 监听 二,服务器启动 2.1 获取连接 2.2 提供服务 2.3 客户端启动源文件 Main.cc 二,客户端编写 2.1 关于Tcp客户端 2.2 客户端代码 2.3 效果…...

简单的Linux Ftp服务搭建
简单的Linux FTP服务搭建 1.需求 公司有一个esb文件传输代理,其中我们程序有文件传输功能,需要将本地文件传输到esb文件代理服务器上,传输成功之后发送http请求,告知esb将固定文件进行传输到对应外围其他服务的文件目录中&#…...

SQL的高级查询练习知识点(day24)
目录 1 学习目标 2 基础查询 2.1 语法 2.2 例子 3 条件查询 3.1 含义 3.2 语法 3.3 条件表达式 3.3.1 条件运算符 3.3.2 例子 3.4 逻辑表达式 3.4.1 逻辑运算符 3.4.2 例子 3.5 模糊查询 3.5.1 概述 3.5.2 例子 4 DISTINCT关键字 4.1 含义 4.2 例子 5 总结…...

Python条件表达式优化的10个实例
Python 中的条件表达式(也称为三元运算符)是一种简洁的语法,用于在单个表达式中执行 if-else 逻辑。虽然它们本身并不直接“优化”代码的执行速度,但它们可以使代码更加简洁、易读,并且有助于避免不必要的嵌套或复杂的…...

oatpp apiclient 客户端get,post请求python fastapi demo
最新用fastapi搞了个服务端,python功能太强了,就是环境不好弄,弄好后,不要轻易换python版本,不要装多个python版本 前面搞了个oatpp webapi服务端,现在要用客户端,为什么用opatpp客户端,因为他不再带其他库了 demo: 我的请求比较简单,就是向python 的 fastapi服务端…...

RK3568平台(内存篇)EMMC介绍
一.eMMC是什么 eMMC (Embedded Multi Media Card)是MMC协会订立、主要针对手机或平板电脑等产品的内嵌式存储器标准规格。由一个嵌入式存储解决方案组成,带有MMC(多媒体卡)接口、快闪存储器设备及主控制器。所有都在一个小型的BGA 封装。接口速度高达每秒52MBytes,eMMC具…...

Python批量读取身份证信息录入系统和重命名
前言 大家好, 如果你对自动化处理身份证图片感兴趣,可以尝试以下操作:从身份证图片中快速提取信息,填入表格并提交到网页系统。如果你无法完成这个任务,我们将在“Python自动化办公2.0”课程中详细讲解实现整个过程。…...

IBM Storwize V7000存储控制器故障节点报错574
背景:由于客户机房搬迁,需要下电迁移设备。该存储自2016年投入生产使用后,从未关过机,已正常运行七八年时间,期间只更换过硬盘,无其他硬件故障。 在GUI界面点击关闭系统后,大概等了40分钟&…...

通信工程学习:什么是SSB单边带调制、VSB残留边带调制、DSB抑制载波双边带调制
SSB单边带调制、VSB残留边带调制、DSB抑制载波双边带调制 SSB单边带调制、VSB残留边带调制、DSB抑制载波双边带调制是三种不同的调制方式,它们在通信系统中各有其独特的应用和特点。以下是对这三种调制方式的详细解释: 一、SSB单边带调制 1、SSB单边带…...

MapSet之二叉搜索树
系列文章: 1. 先导片--Map&Set之二叉搜索树 2. Map&Set之相关概念 目录 前言 1.二叉搜索树 1.1 定义 1.2 操作-查找 1.3 操作-新增 1.4 操作-删除(难点) 1.5 总体实现代码 1.6 性能分析 前言 TreeMap 和 TreeSet 是 Java 中基于搜索树实现的 M…...

OpenCV图像分割教程
OpenCV 图像分割教程 OpenCV 是一个非常强大的计算机视觉库,支持各种图像处理任务。图像分割是 OpenCV 支持的一个重要功能,它用于将图像划分为不同的区域,识别感兴趣的部分。我们将通过介绍 OpenCV 中的图像分割方法,包括基础功…...

python科学计算:NumPy 线性代数与矩阵操作
1 NumPy 中的矩阵与数组 在 NumPy 中,矩阵实际上是一种特殊的二维数组,因此几乎所有数组的操作都可以应用到矩阵上。不过,矩阵运算与一般的数组运算存在一定的区别,尤其是在点积、乘法等操作中。 1.1 创建矩阵 矩阵可以通过 Nu…...

Unity面向对象补全计划 之 List<T>与class(非基础)
C# & Unity 面向对象补全计划 泛型-CSDN博客 关于List,其本质就是C#封装好的一个数组,是一个很好用的轮子,所以并不需要什么特别说明 问题描述 假设我们有一个表示学生的类 Student,每个学生有姓名和年龄两个属性。我们需要创…...

ant design vue+vue3+ts+xlsx实现表格导出问excel文件(带自定义表头)~
1、首先默认你已安装ant design vue、xlsx 库、及file-saver。 2、导入: import * as XLSX from xlsx; import { saveAs } from file-saver; 注:这里的xlsx导入不能这么写,否则会报错,原因是版本不一致,语法向上兼容…...

基于Python爬虫的淘宝服装数据分析项目
文章目录 一.项目介绍二.爬虫代码代码分析 三. 数据处理四. 数据可视化 一.项目介绍 该项目是基于Python爬虫的淘宝服装数据分析项目,以致于帮助商家了解当前服装市场的需求,制定更加精确的营销策略。首先,需要爬取淘宝中关于服装的大量数据…...

Tomcat控制台乱码问题已解决(2024/9/7
步骤很详细,直接上教程 问题复现: 情景一 情景二 原因简述 这是由于编码不一致引起的,Tomcat启动后默认编码UTF-8,而Windows的默认编码是GBK。因此你想让其不乱码,只需配置conf\logging.properties的编码格式即可 解决…...

vue通过html2canvas+jspdf生成PDF问题全解(水印,分页,截断,多页,黑屏,空白,附源码)
前端导出PDF的方法不多,常见的就是利用canvas画布渲染,再结合jspdf导出PDF文件,代码也不复杂,网上的代码基本都可以拿来即用。 如果不是特别追求完美的情况下,或者导出PDF内容单页的话,那么基本上也就满足业…...

服务器数据恢复—Raid磁盘阵列故障类型和常见故障原因
出于尽可能避免数据灾难的设计初衷,RAID解决了3个问题:容量问题、IO性能问题、存储安全(冗余)问题。从数据恢复的角度讨论RAID的存储安全问题。 常见的起到存储安全作用的RAID方案有RAID1、RAID5及其变形。基本设计思路是相似的:当部分数据异…...

C++字符串中的string类操作
愿我如星君如月,夜夜流光相皎洁。 ——《车逍遥篇》【宋】范成大 目录 正文: 主要特点: 基本操作: 代码演示: 总结: 今天我们接着上次的章节继续,这次我们来说一个为解决上个方法的缺陷而诞…...

axios设置responseType: ‘blob‘,获取接口返回的错误信息
在axios的请求中当后端接口返回的是文件流的情况下,我们需要在请求参数里面设置responseType: blob,如果接口报错,默认前端无法获取后端返回的错误信息。 解决方法:通过FileReader获取错误信息 async handleFetch() {const res aw…...

【C++】:模板初阶—函数模板|类模板
✨ Blog’s 主页: 白乐天_ξ( ✿>◡❛) 🌈 个人Motto:他强任他强,清风拂山岗! 💫 欢迎来到我的学习笔记! 本文参考博客:一同感受C模版的所带来的魅力 一、泛型编程思想 首先…...

Java 远程执行服务器上的命令
在Java中使用JSch库执行远程服务器上的命令是一种常见的做法,特别是在需要自动化运维任务或者进行远程文件操作时。以下是基于Codekru网站提供的示例,展示如何使用JSch库在远程服务器上执行单个或多个命令。 准备工作 首先,确保您的项目中已…...

3DMax基础- 创建基础模型
目录 零.软件简介 一. 标准基本型 长方体 圆锥体 球体 圆柱体 管状体 圆环 四棱锥 茶壶 平面编辑 加强型文本 二. 扩展基本体 三.复合对象 变形 散布 一致 连接 图形合并 布尔 并集 合并 交集 差集 四.门和窗 门 窗 植物,栏杆,墙 零.软件简介 3…...

JavaScript 知识点(从基础到进阶)
🌏个人博客主页:心.c 前言:JavaScript已经学完了,和大家分享一下我的笔记,希望大家可以有所收获,花不多说,开干!!! 🔥🔥ǵ…...

计算机网络知识点复习——TCP协议的三次握手与四次挥手(连接与释放)
TCP协议的三次握手与四次挥手(连接与释放) 一、前言二、简单的知识准备1. TCP协议的主要特点2. TCP报文段 三、TCP连接的建立(三次握手)四、TCP连接的释放(四次挥手)五、TCP连接与释放的总结六、结束语 一、…...

SpringDataJPA系列(7)Jackson注解在实体中应用
SpringDataJPA系列(7)Jackson注解在实体中应用 常用的Jackson注解 Springboot中默认集成的是Jackson,我们可以在jackson依赖包下看到Jackson有多个注解 一般常用的有下面这些: 一个实体的示例 测试方法如下: 按照上述图片中的序号做个简…...

【Spring Boot 3】【Web】统一封装 HTTP 响应体
【Spring Boot 3】【Web】统一封装 HTTP 响应体 背景介绍开发环境开发步骤及源码工程目录结构总结背景 软件开发是一门实践性科学,对大多数人来说,学习一种新技术不是一开始就去深究其原理,而是先从做出一个可工作的DEMO入手。但在我个人学习和工作经历中,每次学习新技术总…...

Linux如何做ssh反向代理
SSH反向代理是一种通过SSH协议实现的安全远程访问方式,它允许客户端通过SSH连接到一台具有公网IP的代理服务器,然后这台代理服务器再将请求转发给内部网络中的目标主机。以下是实现SSH反向代理的步骤: 一、准备工作 确保服务器配置ÿ…...

Verilog语法+:和-:有什么用?
Verilog语法:和-:主要用于位选择,可以让代码更简洁。 一、位选择基础 在Verilog中,位选择可以通过直接索引来实现,例如: reg [7:0] data; wire select_a; wire [2:0] select_b; assign select_a data[3]; assign select_b …...