网站平台建设思路,网页设计制作心得体会,沈阳网红餐厅,微信漫画网站模板vc-align源码分析
源码地址#xff1a;https://github.com/vueComponent/ant-design-vue/tree/main/components/vc-align
1 基础代码
1.1 名词约定
需要对齐的节点叫source#xff0c;对齐的目标叫target。
1.2 props
提供了两个参数#xff1a;
align#xff1a;对…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 PropTypeAlignType,required: true},target: {type: [Object, Function] as PropTypeTargetType,required: true}},setup(props, { slots }) {const nodeRef refHTMLElement | 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
// sourceNodes 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对象的详细配置
NameTypeDescriptionpointsString[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 可以优化的点
我们给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 refMonitorRef({cancel: () {},
});
// Listen for source updated
const sourceResizeMonitor refMonitorRef({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: ComputedRefnumber) {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 cloneElementT, U(vnode: VNodeT, U | VNodeT, U[],nodeProps: Recordstring, any OmitVNodeProps, ref { ref?: VNodeProps[ref] | RefObject } {},override true,mergeRef false,
): VNodeT, U {let ele vnode;if (Array.isArray(vnode)) {ele filterEmpty(vnode)[0];}if (!ele) {return null;}const node cloneVNode(ele as VNodeT, 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包裹同时也拿到了它的引用进行定位。