网站建设方案备案,720全景网站怎么做,宁波网络推广渠道有哪些,wordpress get_option arrayPopupInner源码分析 – ant-design-vue系列
1 综述
上一篇讲解了vc-align的工作原理#xff0c;也就是对齐是如何完成的。这一篇主要讲述包裹 Align的组件#xff1a;PopupInner组件是如何工作的。
PopupInner主要是对动画状态的管理#xff0c;比如打开弹窗的时候#…PopupInner源码分析 – ant-design-vue系列
1 综述
上一篇讲解了vc-align的工作原理也就是对齐是如何完成的。这一篇主要讲述包裹 Align的组件PopupInner组件是如何工作的。
PopupInner主要是对动画状态的管理比如打开弹窗的时候弹出的动画是从上到下的关闭的时候正好相反。我们把动画时长改成两秒进行观察。 **动画的管理更深层次来说是对元素类名的管理。**接下来需要搞清楚在动画的过程中状态是如何发生变化的。
2 极简代码
使用vue3提供的动画组件Transition来包裹Align组件但是没有对Transition组件设置任何属性所以暂时还没有动画效果。
return () (Transition{props.visible ? (Align align{props.align} target{props.target}{slots.default?.()}/Align) : null}/Transition
);3 源码实现
3.1 前置知识
组件Transitionhttps://cn.vuejs.org/guide/built-ins/transition.html#transition-on-appear
3.2 PopupInner组件定义的动画状态 源码地址https://github.com/vueComponent/ant-design-vue/blob/main/components/vc-trigger/Popup/useVisibleStatus.ts
在源码中有这样一个hook监控visible的变化并且提供了当前状态和修改状态的函数。doMeasure 是另一个hook提供的函数目的是得到source节点的宽/高度。 // Status const [status, goNextStatus] useVisibleStatus(visible, doMeasure);在useVisibleStatus函数中注释如下 /*** 每个组件正确工作的步骤如下弹出窗口应遵循这个流程。* measure - 检查拉伸尺寸值* align - 让组件对齐位置* aligned - 再次重新对齐以防止其他className更改了大小这一步后续再解释* afterAlign - 选择下一步是触发运动还是结束* beforeMotion - 应将motion重置为invisible以便CSSMotion可以进行正常运动* motion - 执行动作* stable 一切结束*/对源码进行拆解
整体框架
type PopupStatus null | measure | align | aligned | motion | stable;
type Func () void;
const StatusQueue: PopupStatus[] [measure, align, null, motion];export default (visible: Refboolean,doMeasure: Func,
): [RefPopupStatus, (callback?: () void) void] {// 弹出状态const status refPopupStatus(null);// raf (callback: FrameRequestCallback) window.requestAnimationFrame(callback);// rafRef 就是requestAnimationFrame执行器的引用这个hook不做拆解了。const rafRef refnumber();// 标记组件的销毁状态const destroyRef ref(false);const goNextStatus () {// ......}onMounted(() {// ......})return [status, goNextStatus];
}状态驱动如图所示 监控visible的变化
当visible变化的时候重新从measure状态开始流转。
watch(visible,() {setStatus(measure);},{ immediate: true, flush: post },
);监听status的变化
在组件挂载时监控了status的变化。
如果当前是measure调用传入的doMeasure测量target的大小。接下来会设置新状态
如果status measure则执行setStatus(align)如果status align这里因为要多次定位所以nextStatus为空也就是无法自动进入下个状态如果status motionnextStatus同样为空无法自动进入下个状态
所以当popup处于align和motion状态时都需要等待动作完成由外部调用goNextStatus来进入下个状态。
onMounted(() {// Go next statuswatch(status,() {switch (status.value) {case measure:doMeasure();break;default:}if (status.value) {rafRef.value raf(async () {const index StatusQueue.indexOf(status.value);const nextStatus StatusQueue[index 1];if (nextStatus index ! -1) {setStatus(nextStatus);}});}},{ immediate: true, flush: post },);
});function setStatus(nextStatus: PopupStatus) {if (!destroyRef.value) {status.value nextStatus;}
}goNextStatus 函数
可以看到正好弥补了上一个watch不能自动触发的状态变更。
function goNextStatus(callback?: () void) {// 取消原先的动作注册新的。目的是为了动画的流程。cancelRaf();rafRef.value raf(() {// Only align should be manually triggerlet newStatus status.value;switch (status.value) {case align:newStatus motion;break;case motion:newStatus stable;break;default:}setStatus(newStatus);callback?.();});
}其他部分
都是一些清理函数。
onBeforeUnmount(() {destroyRef.value true;cancelRaf();
});function cancelRaf() {raf.cancel(rafRef.value);
}3.3 核心变量
先看一下渲染函数最重要的就是transitionProps和mergedStyle其他变量暂时不管。
return (Transition// ......{...transitionProps}v-slots{{default: () {return !destroyPopupOnHide || props.visible ? (Align// ......v-slots{{default: () (div// ......style{mergedStyle}{childNode}/div),}}/Align) : null;},}}/Transition
);对代码进行debug观察这两个变量在动画状态过程中的变化。可以看到在第一次measure过程结束后:
const transitionProps {name: ant-slide-up,appear: true,enterFromClass: ant-slide-up-enter ant-slide-up-enter-prepare,enterActiveClass: ant-slide-up-enter ant-slide-up-enter-prepare,enterToClass: ant-slide-up-enter ant-slide-up-enter-active,leaveFromClass: ant-slide-up-leave,leaveActiveClass: ant-slide-up-leave ant-slide-up-leave-active,leaveToClass: ant-slide-up-leave ant-slide-up-leave-active
};const mergedStyle [{ minWidth: 76px, opacity: 0, pointerEvents: none }, null];// 后续opacity会变成null弹窗逐步显现。从源码中可以清晰的看到opacity: statusValue motion || statusValue stable || !visible.value ? null : 0,pointerEvents: !visible.value statusValue ! stable ? none : null,这个就是动画的核心了其他代码都是为这个服务的。比如对齐popup的位置控制内部status状态进而让动画在对齐完成后再开始。
动画的源码https://github.com/vueComponent/ant-design-vue/blob/main/components/style/core/motion/slide.less
3.4 代码详解
由于代码逻辑都穿插在一起所以先把所有逻辑分开看一下最后在用一个流程进行串联。
遵照源码对代码的分块。
3.4.1 Measure
// Measure
/**
* 如果 stretch 是width那么stretchStyle返回元素的 width如果stretch 是minWidth那么stretchStyle返回元素的 minWidth
* height也是一样的。
*
* 开始时 width height 0px
* 只有调用measureStretchStyle才能获得目标元素的属性
*/
const [stretchStyle, measureStretchStyle] useStretchStyle(toRef(props, stretch));const doMeasure () {if (props.stretch) {measureStretchStyle(props.getRootDomNode());}
};const visible ref(false);
let timeoutId: any;
watch(() props.visible,val {clearTimeout(timeoutId);if (val) {/*** 如果visible变成true那么延迟变更*/timeoutId setTimeout(() {visible.value props.visible;});} else {visible.value false;}},{ immediate: true },
);3.4.2 Aligns // Aligns
/**
* 解释见下文
*/
const prepareResolveRef ref(value?: unknown) void();/**
* Align组件的target可以接受函数或者对象获取的时候也要区分。
*/
const getAlignTarget () {if (props.point) {return props.point;}return props.getRootDomNode;
};/**
* 调用Align组件的对齐方法
*/
const forceAlign () {alignRef.value?.forceAlign();
};/**
* popupDomNode是弹窗节点matchAlign是对齐的结果reuslt
* 使用align-dom对齐会有自适应视口的位置调整因此result并不一定是是设置的位置
*/
const onInternalAlign (popupDomNode: HTMLElement, matchAlign: AlignType) {/*** 根据对齐结果获取类名比如默认的左上角对齐类名是 ant-dropdown-placement-bottomLeft*/const nextAlignedClassName props.getClassNameFromAlign(matchAlign);const preAlignedClassName alignedClassName.value;if (alignedClassName.value ! nextAlignedClassName) {alignedClassName.value nextAlignedClassName;}/*** 如果当前状态是align并且新的类名和旧的不同也就是对齐方式不同就重新执行对齐方法拿到新的对齐结果再比较* 直到对齐无误后status进入下个阶段motion同时放开动画进入动画的下个阶段*/if (status.value align) {// Repeat until not more align neededif (preAlignedClassName ! nextAlignedClassName) {Promise.resolve().then(() {forceAlign();});} else {goNextStatus(() {prepareResolveRef.value?.();});}props.onAlign?.(popupDomNode, matchAlign);}
};prepareResolveRef
是一个Promise的resolve方法执行这个方法可以让Promise结束。这个promise的定义在Motion模块。
/**
* 从名字可以看出这个是动画开始前的一个阶段。
*/
const onShowPrepare () {return new Promise(resolve {prepareResolveRef.value resolve;});
};使用的地方在Transition 组件也就是说onBeforeEnter要想结束需要等到prepareResolveRef.value?.()的执行。
Transition// ......onBeforeEnter{onShowPrepare}
/TransitiononInternalAlign
在Align组件中有以下这句代码。执行的就是onInternalAlign这个函数传入source和result
if (latestOnAlign result) {latestOnAlign(source, result);
}3.4.3 Motion
// Motion
const motion computed(() {/*** 如果设置了动画就使用设置好的否则使用默认的。也就是3.3 展示的变量*/const m typeof props.animation object ? props.animation : getMotion(props as any);[onAfterEnter, onAfterLeave].forEach(eventName {/*** 拦截了两个状态* 如果动画状态是onAfterEnter或者onAfterLeave也就是打开和关闭的结束时间点。* 就手动把status状态设置为stable同时执行原动画逻辑。*/const originFn m[eventName];m[eventName] node {goNextStatus();// 结束后强制 stablestatus.value stable;originFn?.(node);};});return m;
});const onShowPrepare () {return new Promise(resolve {prepareResolveRef.value resolve;});
};/**
* 如果不需要动画且status是动画状态则直接进入下个阶段stable
*/
watch([motion, status],() {if (!motion.value status.value motion) {goNextStatus();}},{ immediate: true },
);expose({forceAlign,getElement: () {return (elementRef.value as any).$el || elementRef.value;},
});const alignDisabled computed(() {if ((props.align as any)?.points (status.value align || status.value stable)) {return false;}return true;
});3.5 主流程讲解 Align组件使用了v-show属性可以应用Transition的动画效果。由visible变量控制。当visible变成true时触发了useVisibleStatus这个hookstatus状态开始流转。在useVisibleStatus这个hook中status被设置成了measure还是在这个hook中因为status被watch监听因此触发了回调执行了doMeasure方法算出popup的样式同时status被推到align此时再次触发useVisibleStatus这个hook中status的监听状态被推到了null也就是这个hook暂时不能控制状态了。因为doMeasure方法的执行source的大小发生了变化对齐方法forceAlign开始执行详情请看上一篇。每次forceAlign执行的末尾都会执行latestOnAlign(source, result);对应的就是PopupInner中的onInternalAlign方法。这个方法中会多次调用forceAlign()直到source被准确定位排除各种视口变化对位置的影响以便动画的位置没有错误。当位置稳定后onInternalAlign会手动将状态推进到下一步motion。同时执行prepareResolveRef.value?.()让css动画也进入下一个阶段。如果动画执行完成通过劫持动画的onAfterEnter阶段推进状态到stable到此一次动画结束。
4 总结
本篇介绍了PopupInner的源码实现由于状态变化的复杂所以只要理解流程即可在实际开发中我们的对齐定位大多不会如此复杂。如果只允许超着一个方向对齐那么只要为Transtion设置类名即可。