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

如何使用watchEffect()防止被中止的异步请求破坏组件状态

问题场景

组件通过prop接收参数resourceId,需要从API拉取对应资源并渲染,同时处理加载态、错误态,实现模式参考Vue Router官方导航后数据拉取方案。
API拉取函数会同步返回abort中止函数,调用后当前请求会立即抛出拒绝结果。需求要求:当resourceId发生变化时,所有进行中的请求必须中止,转而发起新的请求,本次实现基于fetch()与AbortController完成请求中止能力。

初始实现与异常表现

基于Vue3组合式API的初始实现代码如下:

const loading = ref(false);
const data = ref(null);
const error = ref(null);

watchEffect(async (onCancel) => {
    loading.value = true;
    data.value = error.value = null;

    const { response, abort } = fetchResourceFromApi(props.resourceId);
    onCancel(abort);

    try {
        data.value = await (await response).json();
    } catch (e) {
        error.value = e;
    } finally {
        loading.value = false;
    }
});

对应模板代码:

<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error }}</div>
<div v-else>{{ data }}</div>

上述代码在单请求场景下运行正常,但在快速切换resourceId触发请求取消时会出现状态混乱:当上一个API请求未完成时resourceId就发生变化,事件会按以下顺序执行:

  • 旧请求的abort()被调用
  • watchEffect回调重新执行,重置loading、error、data的状态值
  • 旧请求的catch和finally代码块执行,修改loading和error的值
  • 新发起的API请求完成,设置loading和data的值

最终会出现不符合预期的异常状态:第二个请求仍在加载过程中时loading就被设为false,error中存储了第一个请求被中止抛出的异常,虽然最终data能拿到第二个请求的返回值,但中间状态完全混乱。

解决方案

问题核心原因是被主动中止的旧请求,其异步回调仍然持有响应式状态的修改权限,会在请求作废后覆盖新请求设置的初始状态。解决思路很明确:给每个请求增加作废标记,只有未被作废的最新请求,才允许修改全局响应式状态。

方案1:请求ID校验(适合多请求竞态场景)

通过自增ID标记每次请求的优先级,只有ID匹配最新请求的回调才允许修改状态:

const loading = ref(false);
const data = ref(null);
const error = ref(null);
// 记录最新请求的标识
let latestRequestId = 0;

watchEffect(async (onCancel) => {
    loading.value = true;
    data.value = error.value = null;
    // 为当前请求生成唯一ID,更新为最新请求标识
    const currentRequestId = ++latestRequestId;

    const { response, abort } = fetchResourceFromApi(props.resourceId);
    onCancel(abort);

    try {
        const res = await (await response).json();
        // 仅当前请求是最新请求时,才更新数据
        if (currentRequestId === latestRequestId) {
            data.value = res;
        }
    } catch (e) {
        // 忽略主动中止产生的错误,且仅最新请求可更新错误状态
        if (currentRequestId === latestRequestId && e.name !== 'AbortError') {
            error.value = e;
        }
    } finally {
        // 仅最新请求可更新加载状态
        if (currentRequestId === latestRequestId) {
            loading.value = false;
        }
    }
});

方案2:内部取消标记(逻辑更内聚)

不需要维护全局ID,在每个watchEffect回调内部维护取消标记即可,更贴合onCancel的设计逻辑:

const loading = ref(false);
const data = ref(null);
const error = ref(null);

watchEffect(async (onCancel) => {
    loading.value = true;
    data.value = error.value = null;
    // 标记当前请求是否已被作废
    let isCancelled = false;

    const { response, abort } = fetchResourceFromApi(props.resourceId);
    onCancel(() => {
        isCancelled = true;
        abort();
    });

    try {
        const res = await (await response).json();
        if (!isCancelled) {
            data.value = res;
        }
    } catch (e) {
        if (!isCancelled && e.name !== 'AbortError') {
            error.value = e;
        }
    } finally {
        if (!isCancelled) {
            loading.value = false;
        }
    }
});

两种方案都可以彻底解决旧请求回调覆盖状态的问题,其中方案2实现更简洁,不需要额外维护全局变量,是单资源拉取场景下的首选实现。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 02:06:23