Built-in composition APIs of Vant.
onMountedOrActivated
- onMountedOrActivated
优点
- 合并钩子函数
- 封装 mounted 状态在 hook 内部
export function onMountedOrActivated(hook: () => any) {
let mounted: boolean; // 首次加载不触发onActivated钩子,防止触发两次事件
onMounted(() => {
hook();
nextTick(() => {
mounted = true;
});
});
// 请注意:
// onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。
// 这两个钩子不仅适用于 <KeepAlive> 缓存的根组件,也适用于缓存树中的后代组件。
// https://cn.vuejs.org/guide/built-ins/keep-alive.html#basic-usage
onActivated(() => {
if (mounted) {
hook();
}
});
}
useEventListener
- useEventListener 事件监听
优点
- 响应式的 target 节点变化会自动更新监听事件到新节点上
- 自动挂载卸载监听事件
import { Ref, watch, isRef, unref, onUnmounted, onDeactivated } from 'vue';
import { onMountedOrActivated } from '../onMountedOrActivated';
import { inBrowser } from '../utils';
type TargetRef = EventTarget | Ref<EventTarget | undefined>;
export type UseEventListenerOptions = {
target?: TargetRef;
capture?: boolean; // 默认情况下,capture 参数为 false,表示事件处理程序在冒泡阶段执行。如果将 capture 参数设置为 true,事件处理程序将在捕获阶段执行。
passive?: boolean; // 用于指示事件处理程序是否是被动模式。在被动模式下,事件处理程序不会调用 preventDefault() 来阻止事件的默认行为。这对于一些滚动事件等性能敏感的事件非常有用,因为它可以告诉浏览器该事件处理程序不会阻止滚动,从而优化滚动的性能。
};
// 函数重载
export function useEventListener<K extends keyof DocumentEventMap>(
type: K,
listener: (event: DocumentEventMap[K]) => void,
options?: UseEventListenerOptions
): void;
export function useEventListener(
type: string,
listener: EventListener,
options?: UseEventListenerOptions
): void;
export function useEventListener(
type: string,
listener: EventListener,
options: UseEventListenerOptions = {}
) {
// 非浏览器环境直接返回
if (!inBrowser) {
return;
}
const { target = window, passive = false, capture = false } = options;
let attached: boolean; // 防止多次挂载以及卸载状态
const add = (target?: TargetRef) => {
const element = unref(target);
if (element && !attached) {
element.addEventListener(type, listener, {
capture,
passive,
});
attached = true;
}
};
const remove = (target?: TargetRef) => {
const element = unref(target);
if (element && attached) {
element.removeEventListener(type, listener, capture);
attached = false;
}
};
onUnmounted(() => remove(target));
onDeactivated(() => remove(target));
// 这是不是也可以写一个onUnmountedOrDeactivated
onMountedOrActivated(() => add(target));
// 实时更新监听函数
if (isRef(target)) {
watch(target, (val, oldVal) => {
remove(oldVal);
add(val);
});
}
}
useClickAway
优点
- 功能封装
- 自动挂载卸载监听事件
- useClickAway 点击在元素外部
targeteventName没有watch监听,不会响应式变化更新到- 核心利用了 contains 属性做判断有无包含在内
import { Ref, unref } from 'vue';
import { inBrowser } from '../utils';
import { useEventListener } from '../useEventListener';
export type UseClickAwayOptions = {
eventName?: string;
};
export function useClickAway(
target:
| Element
| Ref<Element | undefined>
| Array<Element | Ref<Element | undefined>>,
listener: EventListener,
options: UseClickAwayOptions = {}
) {
// 非浏览器环境直接返回
if (!inBrowser) {
return;
}
const { eventName = 'click' } = options;
const onClick = (event: Event) => {
const targets = Array.isArray(target) ? target : [target];
// 是否全部节点都不包含点击的节点
const isClickAway = targets.every((item) => {
const element = unref(item);
return element && !element.contains(event.target as Node);
// contains() 表示一个节点是否是给定节点的后代,即该节点本身、其直接子节点(childNodes)、子节点的直接子节点等
});
// 满足不包含目标元素则触发回调
if (isClickAway) {
listener(event);
}
};
// 监听事件
// useEventListener options
useEventListener(eventName, onClick, { target: document });
}
useCountDown
- useCountDown 倒计时器
优点
- 更准确的倒计时使用,支持毫秒级和秒级倒计时
- 组件卸载时自动清除倒计时事件
- 控制事件全暴露给外部调用
参数
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| time | 倒计时时长,单位毫秒 | number | - |
| millisecond | 是否开启毫秒级渲染 | boolean | false |
| onChange | 倒计时改变时触发的回调函数 | (current: CurrentTime) => void | - |
| onFinish | 倒计时结束时触发的回调函数 | - |
原理
- 手动触发
start函数 计算endTime确定什么时候结束计时 和 变更counting状态 - 区分是否毫秒计时器计时
- 计时器通过
raf递归时间,相比下setInterval会被长任务阻塞导致不准
requestAnimationFrame cancelAnimationFrame API
window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
window.cancelAnimationFrame 取消一个先前通过调用 window.requestAnimationFrame()方法添加到计划中的动画帧请求。
以下是 requestAnimationFrame 的优势
-
更加流畅更好的性能和节能:requestAnimationFrame 会在浏览器的重绘周期内执行回调函数,通常是每秒 60 次(具体频率取决于浏览器和设备)。它会与浏览器的渲染过程同步,确保在最佳的时机执行代码,以实现平滑的动画效果。相比之下,setInterval 的执行频率是固定的,可能会导致过早或过晚执行代码,从而产生不稳定的动画效果。此外,requestAnimationFrame 在页面不可见或处于后台标签页时会自动暂停,节省了不必要的计算资源。
-
自动适应帧率:requestAnimationFrame 的回调函数会接收一个时间戳参数,可以利用这个时间戳计算动画的进度和变化。这样可以根据实际的渲染帧率来调整动画的速度和流畅度,使其在不同的设备和浏览器上都表现出一致的效果。而 setInterval 固定的时间间隔可能会导致动画在不同设备上的表现不一致。
-
避免卡顿和掉帧:requestAnimationFrame 会在浏览器的重绘周期内执行代码,避免了连续执行大量计算造成的卡顿和掉帧现象。相比之下,setInterval 的代码执行不受浏览器的渲染周期限制,可能会在某些情况下导致页面响应变慢或出现明显的卡顿。
源码如下
let rafId: number; // 计时器ID
let endTime: number; // 结束时间
let counting: boolean; // 是否在计时
let deactivated: boolean; // 是否在后台
// 获取倒计时时长
const remain = ref(options.time);
// 实时计算格式化返回值
const current = computed(() => parseTime(remain.value));
// 直接利用剩余时间来记录是否结束计时器
// 1 不好拿刷新率间隔
// 2 实时算准确度更好
const getCurrentRemain = () => Math.max(endTime - Date.now(), 0); // 获取当前剩余时间
// 设置剩余时间
// 不停设置更新remain状态值,为零停止
const setRemain = (value: number) => {
remain.value = value;
options.onChange?.(current.value);
if (value === 0) {
pause();
options.onFinish?.();
}
};
// 毫秒级别的计时器
// 利用 raf 递归调用,直至剩余时间为0停止
const microTick = () => {
rafId = raf(() => {
// in case of call reset immediately after finish
// 为什么需要 counting 状态?
// cancelAnimationFrame 是用于取消由 requestAnimationFrame 方法创建的动画帧请求的函数。它通过传递给 requestAnimationFrame 的唯一标识符(即请求的帧ID)来取消计划的动画帧。然而,cancelAnimationFrame 并不能确保计时器被正确地停止更新的原因是,它只能取消尚未执行的动画帧请求。当你调用 requestAnimationFrame 时,浏览器会在下一次重绘之前执行指定的回调函数。如果在这之前调用了 cancelAnimationFrame,则可以阻止回调函数的执行。
// 然而,如果回调函数已经开始执行并且处于动画循环中,调用 cancelAnimationFrame 将无法停止该循环。这是因为 cancelAnimationFrame 只会取消尚未执行的动画帧请求,并不能中断正在进行的帧循环。
if (counting) {
setRemain(getCurrentRemain());
if (remain.value > 0) {
microTick();
}
}
});
};
// 秒级别的计时器
const macroTick = () => {
rafId = raf(() => {
// in case of call reset immediately after finish
if (counting) {
const remainRemain = getCurrentRemain();
if (!isSameSecond(remainRemain, remain.value) || remainRemain === 0) {
setRemain(remainRemain);
}
if (remain.value > 0) {
macroTick();
}
}
});
};
// 开始计时
const tick = () => {
// should not start counting in server
// see: https://github.com/vant-ui/vant/issues/7807
if (!inBrowser) {
return;
}
if (options.millisecond) {
microTick();
} else {
macroTick();
}
};
// 第一次开始计时
const start = () => {
if (!counting) {
endTime = Date.now() + remain.value;
counting = true;
tick();
}
};
const reset = (totalTime: number = options.time) => {
pause();
remain.value = totalTime;
};
const pause = () => {
counting = false;
cancelRaf(rafId);
};
onBeforeUnmount(pause);
onActivated(() => {
if (deactivated) {
counting = true;
deactivated = false;
tick();
}
});
onDeactivated(() => {
if (counting) {
pause();
deactivated = true;
}
});
return {
start,
pause,
reset,
current,
};
CountDown组件
Props
| 参数 | 说明 | 类型 | 默认值 |
|---|---|---|---|
| time | 倒计时时长,单位毫秒 | number | string | 0 |
| format | 时间格式 | string | HH:mm:ss |
| auto-start | 是否自动开始倒计时 | boolean | true |
| millisecond | 是否开启毫秒级渲染 | boolean | false |
- 基于
useCountDown上面的封装 - 组件支持响应式
props.time入参的变化实时响应 useExpose暴露事件给用户调用slots.default支持默认插槽渲染
export default defineComponent({
name,
props: countDownProps,
emits: ['change', 'finish'],
setup(props, { emit, slots }) {
const { start, pause, reset, current } = useCountDown({
time: +props.time,
millisecond: props.millisecond,
onChange: (current) => emit('change', current),
onFinish: () => emit('finish'),
});
const timeText = computed(() => parseFormat(props.format, current.value));
const resetTime = () => {
reset(+props.time);
if (props.autoStart) {
start();
}
};
watch(() => props.time, resetTime, { immediate: true });
useExpose({
start,
pause,
reset: resetTime,
});
return () => (
<div role="timer" class={bem()}>
{slots.default ? slots.default(current.value) : timeText.value}
</div>
);
},
});