提升 Vue 项目开发效率:useDelayedRender 实现延迟渲染

1,066 阅读3分钟

Vue 组件开发中,我们经常遇到状态切换的问题,比如:

  • 模态框Modal渐隐渐现
  • 折叠面板Accordion有展开动画
  • 骨架屏Skeleton等待数据加载再显示
  • 动画过渡效果Transition

Vuev-ifv-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?.();
    }
  );
};

主要功能

  1. 监听 indicator 变化,并在切换时触发 beforeShowbeforeHide 钩子。
  2. 通过 nextTick 确保 Vue DOM 更新后才执行状态改变。
  3. 提供 shouldSetIntermediate 让用户自定义是否需要中间状态(如动画过渡)。
  4. 监听 intermediateIndicator,在切换时触发 afterShowafterHide 钩子。

使用示例

处理 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 = truebeforeShow 触发 → intermediateIndicator = true → 渲染组件 → afterShow 触发
  • showModal = falsebeforeHide 触发 → 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 的核心作用:

  1. 控制组件的显示/隐藏逻辑,避免 v-if 直接切换导致动画丢失。
  2. 支持生命周期钩子(beforeShow, afterShow, beforeHide, afterHide),可用于动画、异步请求等操作。
  3. 优化渲染性能,避免不必要的 DOM 操作,适用于懒加载、骨架屏等场景。

🚀 如果你的 Vue 项目涉及动态渲染和动画,这个 Hook 绝对值得一试!