You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue响应式与Angular RxJS:基于定时器的异步UI更新处理

如何在HTTP请求(或其他耗时异步操作)后按规则显示加载Spinner?

需遵循以下逻辑:

  • 等待X时长(示例值:100ms),期间不显示任何内容;
  • 若X时长后数据已返回,立即展示数据;
  • 若X时长后数据未返回,则至少显示Spinner Y时长(示例值:250ms),直至数据返回。

Angular/RxJS 实现方案

在Service中可通过RxJS的流组合实现该逻辑:

/**
 * We want to display the loading indicator only if the requests takes more than `${INITIAL_WAITING_TIME}
 * if it does, we want to wait at least `${MINIMUM_TIME_TO_DISPLAY_LOADER} before emitting
 */
const startLoading$ = of({}).pipe(
  tap(() => this.setState({ loading: true })),
  delay(MINIMUM_TIME_TO_DISPLAY_LOADER),
  switchMap(() => EMPTY)
);

const hideLoading$ = of(null).pipe(
  tap(() => this.setState({ loading: false }))
);

const timer$ = timer(INITIAL_WAITING_TIME).pipe();

/**
 * We want to race two streams:
 *
 * - initial waiting time: the time we want to hold on any UI updates
 * to wait for the API to get back to us
 *
 * - data: the response from the API.
 *
 * Scenario A: API comes back before the initial waiting time
 *
 * We avoid displaying the loading spinner altogether, and instead we directly update
 * the state with the new data.
 *
 * Scenario B: API doesn't come back before initial waiting time.
 *
 * We want to display the loading spinner, and to avoid awkward flash (for example the response comes back 10ms after the initial waiting time) we extend the delay to 250ms
 * to give the user the time to understand the actions happening on the screen.
 */
const race$ = race(timer$, data$).pipe(
  switchMap((winner) =>
    typeof winner === 'number' ? startLoading$ : EMPTY
  )
);

return concat(race$, hideLoading$, data$).pipe(filter(Boolean));

Vue 当前嵌套 setTimeout + watch 的实现

目前的实现依赖嵌套定时器和响应式属性监听,代码如下:

<script setup lang="ts">
import { ref, watch } from 'vue'

const displayUI = ref<boolean>(false);
const loading = ref<boolean>(false);
const data = ref<string | null>(null);
  
setTimeout(() => {
  // 100ms后准备显示UI
  displayUI.value = true;
  
  // 若数据已返回,直接展示并退出
  if (data.value) {
    return;
  }

  // 数据未返回,显示加载状态
  loading.value = true;

  setTimeout(() => {
    // 250ms后检查数据是否返回
    if (data.value) {
      loading.value = false;
    } else {
      // 监听数据返回事件
      watch(data, (value) => {
        if (value) {
          loading.value = false
        }
      })
    }
  }, 250)
    
}, 100) 

// 模拟异步请求
setTimeout(() => {
  data.value = 'Data arrived'
}, 400)
</script>

<template>
  <template v-if="displayUI">
    <h1 v-if="!data && !loading">
      No Data
    </h1>
    <h1 v-if="!loading && data">
      {{ data }}
    </h1>
    <h1 v-if="loading">
      Loading...
    </h1>
  </template>
</template>

Vue 无需 RxJS 的更优实现方案

可以利用原生Promise.race、Promise.all结合async/await实现逻辑,避免嵌套定时器和零散监听,让代码结构更清晰、易维护。

实现思路

  1. 将异步请求包装为Promise;
  2. 用Promise.race对比「初始等待定时器Promise」和「请求Promise」,判断是否需要显示加载;
  3. 若需显示加载,用Promise.all等待「最小显示时长定时器Promise」和「请求Promise」,确保加载状态至少保持Y时长;
  4. 通过响应式变量统一控制UI状态。

代码实现

<script setup lang="ts">
import { ref } from 'vue'

// 配置参数
const INITIAL_WAIT = 100; // 初始等待时长(ms)
const MIN_LOADING_SHOW = 250; // 加载最少显示时长(ms)

// 响应式状态
const displayUI = ref(false);
const loading = ref(false);
const data = ref<string | null>(null);

// 模拟异步请求(实际项目中替换为真实HTTP请求)
const fetchData = () => {
  return new Promise<string>((resolve) => {
    // 可修改此值测试不同场景:比如50ms(场景A)、300ms(场景B)
    setTimeout(() => resolve('Data arrived'), 400);
  });
};

// 核心逻辑
const handleRequest = async () => {
  const requestPromise = fetchData();
  let requestResolved = false;

  // 提前监听请求完成,标记状态并更新数据
  requestPromise.then(res => {
    requestResolved = true;
    data.value = res;
  });

  // 初始等待阶段:判断请求是否在初始等待时间内完成
  const initialWaitPromise = new Promise(resolve => setTimeout(resolve, INITIAL_WAIT));
  await Promise.race([initialWaitPromise, requestPromise]);

  if (requestResolved) {
    // 场景A:请求在初始等待前完成,直接显示数据
    displayUI.value = true;
    return;
  }

  // 场景B:初始等待后请求未完成,显示加载状态
  displayUI.value = true;
  loading.value = true;

  // 确保加载状态至少保持MIN_LOADING_SHOW时长
  const minLoadingPromise = new Promise(resolve => setTimeout(resolve, MIN_LOADING_SHOW));
  await Promise.all([minLoadingPromise, requestPromise]);

  // 加载结束,更新状态
  loading.value = false;
};

// 触发请求
handleRequest();
</script>

<template>
  <template v-if="displayUI">
    <h1 v-if="loading">Loading...</h1>
    <h1 v-else-if="data">{{ data }}</h1>
    <h1 v-else>No Data</h1>
  </template>
</template>

方案优势

  • 逻辑扁平化:避免嵌套定时器和零散监听,代码结构直观;
  • 状态可控:通过Promise组合清晰处理所有场景,减少状态变更的边界问题;
  • 可复用性:核心逻辑可封装为工具函数,在多组件中复用;
  • 符合Vue3组合式API风格,代码易维护、易测试。

内容的提问来源于stack exchange,提问作者floroz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 20:50:31