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实现逻辑,避免嵌套定时器和零散监听,让代码结构更清晰、易维护。
实现思路
- 将异步请求包装为Promise;
- 用
Promise.race对比「初始等待定时器Promise」和「请求Promise」,判断是否需要显示加载; - 若需显示加载,用
Promise.all等待「最小显示时长定时器Promise」和「请求Promise」,确保加载状态至少保持Y时长; - 通过响应式变量统一控制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
相关产品推荐
相关产品推荐

