在 Vue 组件开发中,我们经常遇到状态切换的问题,比如:
- 模态框
Modal渐隐渐现 - 折叠面板
Accordion有展开动画 - 骨架屏
Skeleton等待数据加载再显示 - 动画过渡效果
Transition
而 Vue 的 v-if 或 v-show 并不会等待动画完成后销毁/显示元素,这可能导致视觉上的突兀或布局错乱。
💡 useDelayedRender 就是为了解决这个问题的!
源码
import { nextTick, unref, watch } from 'vue';
import type { Ref } from 'vue';
export type UseDelayedRenderProps = {
/**
* 主要的显示/隐藏状态指示器
* - `true` 表示显示
* - `false` 表示隐藏
*/
indicator: Ref<boolean>;
/**
* 过渡中的中间状态指示器
* - `true` 代表正在过渡到“显示”状态
* - `false` 代表正在过渡到“隐藏”状态
*/
intermediateIndicator: Ref<boolean>;
/**
* 控制是否在状态切换时设置中间状态
* @param step 'show' | 'hide' 当前的切换步骤
* @returns 是否应用中间状态
*/
shouldSetIntermediate?: (step: 'show' | 'hide') => boolean;
/** 在显示 (`indicator` 变为 `true`) 之前触发 */
beforeShow?: () => void;
/** 在隐藏 (`indicator` 变为 `false`) 之前触发 */
beforeHide?: () => void;
/** 在 `intermediateIndicator` 变为 `true` 之后触发(显示完成) */
afterShow?: () => void;
/** 在 `intermediateIndicator` 变为 `false` 之后触发(隐藏完成) */
afterHide?: () => void;
};
/**
* **useDelayedRender**
*
* 该 Hook 用于处理带有 **过渡状态** 的异步渲染逻辑。
*
* 主要作用:
* - 监听 `indicator`(是否显示),并在 `nextTick` 后决定是否更新 `intermediateIndicator`(中间过渡状态)。
* - 允许 `beforeShow` / `beforeHide` 在状态切换前执行回调。
* - 允许 `afterShow` / `afterHide` 在过渡结束后执行回调。
* - 通过 `shouldSetIntermediate` 控制是否应用中间状态,提升渲染灵活性。
*
* @param {UseDelayedRenderProps} options 传入参数,包括 `indicator`、`intermediateIndicator`、回调函数等
*/
export const useDelayedRender = ({
indicator,
intermediateIndicator,
shouldSetIntermediate = () => true,
beforeShow,
beforeHide,
afterShow,
afterHide,
}: UseDelayedRenderProps) => {
watch(
() => unref(indicator),
async (val) => {
const step = val ? 'show' : 'hide';
// 触发前置回调
val ? beforeShow?.() : beforeHide?.();
// 仅在需要设置中间状态时,才执行 nextTick 进行状态切换
if (shouldSetIntermediate(step)) {
await nextTick();
// 避免竞态问题:检查 indicator 是否已被修改
if (unref(indicator) !== val) return;
intermediateIndicator.value = val;
// 触发后置回调
val ? afterShow?.() : afterHide?.();
}
}
);
watch(
() => intermediateIndicator.value,
(val) => {
val ? afterShow?.() : afterHide?.();
}
);
};
主要功能
- 监听
indicator变化,并在切换时触发beforeShow、beforeHide钩子。 - 通过
nextTick确保Vue DOM更新后才执行状态改变。 - 提供
shouldSetIntermediate让用户自定义是否需要中间状态(如动画过渡)。 - 监听
intermediateIndicator,在切换时触发afterShow、afterHide钩子。
使用示例
处理 Modal 显示和动画
在 Modal 组件中,我们希望:
- 显示时:先执行
beforeShow(可以是fadeIn动画),再真正渲染组件。 - 隐藏时:先执行
beforeHide(可以是fadeOut动画),再从DOM移除。
<template>
<button @click="toggleModal">切换模态框</button>
<div v-if="showIntermediate" class="modal" :class="{ show: showModal }">
<p>模态框内容</p>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useDelayedRender } from "./useDelayedRender";
const showModal = ref(false);
const showIntermediate = ref(false);
const toggleModal = () => (showModal.value = !showModal.value);
useDelayedRender({
indicator: showModal,
intermediateIndicator: showIntermediate,
beforeShow: () => console.log("即将显示"),
afterShow: () => console.log("已经显示"),
beforeHide: () => console.log("即将隐藏"),
afterHide: () => console.log("已经隐藏"),
});
</script>
<style>
.modal {
opacity: 0;
transform: translateY(-20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
.modal.show {
opacity: 1;
transform: translateY(0);
}
</style>
效果:
showModal = true→beforeShow触发 →intermediateIndicator = true→ 渲染组件 →afterShow触发showModal = false→beforeHide触发 →intermediateIndicator = false→ 组件销毁 →afterHide触发
处理 Collapse 组件
Collapse 组件(如手风琴菜单)也需要延迟渲染,否则展开动画可能无法生效:
<template>
<button @click="toggle">切换折叠</button>
<div v-if="showIntermediate" class="content" :class="{ expand: isExpanded }">
<p>展开的内容</p>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useDelayedRender } from "./useDelayedRender";
const isExpanded = ref(false);
const showIntermediate = ref(false);
const toggle = () => (isExpanded.value = !isExpanded.value);
useDelayedRender({
indicator: isExpanded,
intermediateIndicator: showIntermediate,
shouldSetIntermediate: (step) => step === "show", // 只在展开时渲染
});
</script>
<style>
.content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
.content.expand {
max-height: 100px;
}
</style>
shouldSetIntermediate('hide') 返回 false,让 Collapse 组件保持在 DOM 里,这样动画就不会被 v-if 打断。
useDelayedRender 的核心作用:
- 控制组件的显示/隐藏逻辑,避免
v-if直接切换导致动画丢失。 - 支持生命周期钩子(
beforeShow,afterShow,beforeHide,afterHide),可用于动画、异步请求等操作。 - 优化渲染性能,避免不必要的
DOM操作,适用于懒加载、骨架屏等场景。
🚀 如果你的 Vue 项目涉及动态渲染和动画,这个 Hook 绝对值得一试!