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

提供了两个参数:

  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 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的实现如下:

  1. 不在回调过程中:直接设置定时
  2. 如果是强制触发:取消旧的定时,设置新的定时
  3. 在回调过程中:取消旧的定时,设置新的定时
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;
};

看一下这个函数的实现。调用了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.mzph.cn/news/878952.shtml

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

相关文章

WPF-快速构建统计表、图表并认识相关框架

一、使用ScottPlot.Wpf 官网地址&#xff1a;https://scottplot.net/quickstart/wpf/ 1、添加NuGet包&#xff1a;ScottPlot.Wpf 2、XAML映射命名空间&#xff1a; xmlns:ScottPlot"clr-namespace:ScottPlot.WPF;assemblyScottPlot.WPF" 3、简单示例&#xff1a;…

2024年测评7款最佳AI论文修改润色平台

在2024年&#xff0c;AI论文修改润色平台的测评和推荐成为学术界和研究者们关注的热点。本文将详细评测并推荐7款最佳AI论文修改润色平台&#xff0c;包括千笔-AIPassPaper&#xff0c;并结合我搜索到的资料进行分析。 一、千笔-AIPassPaper 千笔-AIPassPaper是一款集论文大纲…

【Nginx系列】Nginx中rewrite模块

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

SpringBoot教程(安装篇) | Elasticsearch的安装

SpringBoot教程&#xff08;安装篇&#xff09; | Elasticsearch的安装 一、确定Elasticsearch版本二、下载elasticsearch&#xff08;windows版本&#xff09;官网下载如何解压配置 允许 别人跨域 访问自己启动运行 三、Es可视化工具安装&#xff08;elasticsearch-head&#…

DDS基本原理--FPGA学习笔记

DDS信号发生器原理&#xff1a; timescale 1ns / 1ps // // Company: // Engineer: // // Create Date: 2024/09/04 15:20:30 // Design Name: hilary // Module Name: DDS_Module //module DDS_Module(Clk,Reset_n,Fword,Pword,Data);input Clk;input Reset_n;input [31:0]…

如何使div居中?CSS居中终极指南

前言 长期以来&#xff0c;如何在父元素中居中对齐一个元素&#xff0c;一直是一个让人头疼的问题&#xff0c;随着 CSS 的发展&#xff0c;越来越多的工具可以用来解决这个难题&#xff0c;五花八门的招式一大堆&#xff0c;这篇博客&#xff0c;旨在帮助你理解不同的居中方法…

自制游戏手柄--Android画面的input输入控制

在使用传感器获取到运动数据后&#xff0c;怎样转换为input事件传给手机呢&#xff0c;这里以Android为例&#xff0c; 我们可以考虑以下方式&#xff1a; 1. 物理方式&#xff0c;使用舵机连接触碰笔去实现&#xff0c; 2. 构造MotionEvent事件&#xff0c;注入input&#…

fastadmin 文件上传七牛云

1-安装七牛云官方SDK composer require qiniu/php-sdk 2-七牛云配置 <?phpnamespace app\common\controller;use Qiniu\Storage\BucketManager; use think\Config; use Qiniu\Auth; use Qiniu\Storage\UploadManager; use think\Controller; use think\Db;/*** 七牛基类*…

CTK框架(四): 插件编写

目录 1.生成插件 1.1.环境说明 1.2.服务类&#xff0c;纯虚类&#xff0c;提供接口 1.3.实现插件类&#xff0c;实现纯虚函数 1.4.激活插件&#xff0c;加入ctk框架的生命周期中 1.5.添加资源文件 1.6..pro文件 2.使用此插件 3.总结 1.生成插件 1.1.环境说明 编译ct…

如何将卷积神经网络(CNN)应用于医学图像分析:从分类到分割和检测的实用指南

引言 在现代医疗领域,医学图像已经成为疾病诊断和治疗规划的重要工具。医学图像的类型繁多,包括但不限于X射线、CT(计算机断层扫描)、MRI(磁共振成像)和超声图像。这些图像提供了对身体内部结构的详细视图,有助于医生在进行准确诊断和制定个性化治疗方案时获取关键的信…

[数据结构] 哈希结构的哈希冲突解决哈希冲突

标题&#xff1a;[C] 哈希结构的哈希冲突 && 解决哈希冲突 水墨不写bug 目录 一、引言 1.哈希 2.哈希冲突 3.哈希函数 二、解决哈希冲突 1.闭散列 I&#xff0c;线性探测 II&#xff0c;二次探测 2.开散列 正文开始&#xff1a; 一、引言 哈希表是一种非常实用而…

JS基础学习笔记

1.引入方式 内部脚本 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> <…

Nginx跨域运行案例:云台控制http请求,通过 http server 代理转发功能,实现跨域运行。(基于大华摄像头WEB无插件开发包)

文章目录 引言I 跨域运行案例开发资源测试/生产环境,Nginx代理转发,实现跨域运行本机开发运行II nginx的location指令Nginx配置中, 获取自定义请求header头Nginx 配置中,获取URL参数引言 背景:全景监控 需求:感知站点由于云台相关操作为 http 请求,http 请求受浏览器…

抢鲜体验 PolarDB PG 15 开源版

unsetunsetPolarDB 商业版unsetunset 8 月&#xff0c;PolarDB PostgreSQL 版兼容 PostgreSQL 15 版本&#xff08;商业版&#xff09;正式发布上线。 当前版本主要增强优化了以下方面&#xff1a; 改进排序功能&#xff1a;改进内存和磁盘排序算法。 增强SQL功能&#xff1a;支…

C++笔试强训12、13、14

文章目录 笔试强训12一、选择题1-5题6-10题 二、编程题题目一题目二 笔试强训13一、选择题1-5题6-10题 二、编程题题目一题目二 笔试强训14一、选择题1-5题6-10题 二、编程题题目一题目二 笔试强训12 一、选择题 1-5题 引用&#xff1a;是一个别名&#xff0c;与其被引用的实…

计算机网络(二) —— 网络编程套接字

目录 一&#xff0c;认识端口号 1.1 背景 1.2 端口号是什么 1.3 三个问题 二&#xff0c;认识Tcp协议和Udp协议 三&#xff0c;网络字节序 四&#xff0c;socket编程接口 4.1 socket常见API 4.2 sockaddr结构 一&#xff0c;认识端口号 1.1 背景 问题&#xff1a;在进…

vue2-elementUI-初始化启动项目-git

前置基础 资料下载-阿里云盘 vueaxioselement-uinpmvscode 初始化项目 1.创建vue2工程 1.1 vue create projectName1.2 选择 1.3 初始化 vue-cli 的核心步骤&#xff1a; Manually select features (*) Babel ( ) TypeScript ( ) Progressive Web App (PWA) Support …

【H2O2|全栈】关于HTML(4)HTML基础(三)

HTML相关知识 目录 HTML相关知识 前言 准备工作 标签的具体分类&#xff08;三&#xff09; 本文中的标签在什么位置中使用&#xff1f; 列表 ​编辑​编辑 有序列表 无序列表 自定义列表 表格 拓展案例 预告和回顾 后话 前言 本系列博客将分享HTML相关知识点…

【 html+css 绚丽Loading 】000044 两仪穿行轮

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享htmlcss 绚丽Loading&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495…

4-1.Android Camera 之 CameraInfo 编码模板(前后置摄像头理解、摄像头图像的自然方向理解)

一、Camera.CameraInfo Camera.CameraInfo 是用于获取设备上摄像头信息的一个类&#xff0c;它提供摄像头的各种详细信息&#xff0c;例如&#xff0c;摄像头的方向、是否支持闪光灯等&#xff0c;以下是它的常用属性 static int CAMERA_FACING_BACK&#xff1a;表示设备的后置…