Vue长轮询请求在页面切换时如何正确关闭?

百里树潼 阅读 29

在Vue项目里用长轮询实时获取订单状态,但切换页面时老是报错“Cannot read properties of undefined (reading ‘then’)”。代码是这样写的:


<template>
  <div>订单状态:{{ status }}</div>
</template>

<script>
export default {
  data() {
    return { status: '' };
  },
  mounted() {
    this.poll();
  },
  methods: {
    async poll() {
      try {
        const res = await axios.get('/api/poll', { timeout: 30000 });
        this.status = res.data.status;
        this.poll(); // 递归调用继续轮询
      } catch (err) {
        setTimeout(() => this.poll(), 5000);
      }
    }
  }
}
</script>

我在beforeDestroy里加过this.poll = null,但控制台还是不断报错。页面切换时应该怎样彻底终止这个递归请求呢?

我来解答 赞 4 收藏
二维码
手机扫码查看
2 条解答
FSD-红敏
根本原因是你的递归调用没有设置退出条件,而且在组件销毁后仍然尝试执行后续的异步操作。你给 poll 方法赋 null 根本没用,因为原来的递归调用栈还在继续运行,每个 await 返回后都会继续执行下一次 poll,这时候组件可能已经销毁了,this 就是 undefined。

更严重的是,你在 catch 里还用了 setTimeout 继续轮询,这会导致即使出错也会无限重试,页面切走之后这些请求和定时器全都在后台跑着,内存泄漏都算轻的。

正确的做法分三步:

第一步,加一个标志位来控制是否继续轮询。这样可以在组件销毁时主动中断递归

第二步,在 beforeUnmount 或 beforeDestroy 里清除状态并取消正在进行的请求

第三步,使用 AbortController 来终止还在挂起的网络请求,避免无谓的资源消耗

下面是改好的代码:

export default {
data() {
return {
status: '',
shouldPoll: true, // 控制是否继续轮询
controller: null // 用来取消请求
};
},
mounted() {
this.poll();
},
beforeUnmount() {
// 组件卸载前关闭轮询
this.shouldPoll = false;

// 如果有未完成的请求,直接中止
if (this.controller) {
this.controller.abort();
}
},
methods: {
async poll() {
// 每次轮询前检查是否还需要继续
if (!this.shouldPoll) return;

// 创建新的 AbortController 每次请求独立控制
this.controller = new AbortController();

try {
const res = await axios.get('/api/poll', {
timeout: 30000,
signal: this.controller.signal // 绑定中断信号
});

this.status = res.data.status;

// 只有 shouldPoll 还为 true 才继续下一轮
if (this.shouldPoll) {
this.poll();
}
} catch (err) {
// 请求被中止时不重试
if (err.name === 'AbortError') {
console.log('轮询已中止');
return;
}

// 其他错误比如网络问题,延迟重试
if (this.shouldPoll) {
setTimeout(() => {
this.poll();
}, 5000);
}
}
}
}
}


关键点解释一下:

shouldPoll 这个标志位必须每次递归前都判断,否则没意义。很多人只在最外层判断一次,但你这是递归啊兄弟,每一层都要知道要不要停。

AbortController 是现代浏览器提供的原生机制,axios 支持通过 signal 参数传入。一旦调用 abort(),所有绑定该 signal 的请求会立即以 AbortError 拒绝,不会发出去也不会卡着等超时。

为什么要在每次请求前新建 controller?因为老的已经被 abort 了不能复用,而且每个请求需要独立控制。

顺便提一嘴,长轮询其实不太适合用递归写法,容易爆栈(虽然一般不会),更好的方式是用循环 + 异步等待。不过你现在这个量级问题不大。

最后提醒一点:Vue 2 和 Vue 3 生命周期钩子名字不一样,如果你是 Vue 2 项目,记得把 beforeUnmount 改成 beforeDestroy。

照这么改完,页面切换时不会再报 undefined 的错了,所有资源都能正确释放。我上周刚在生产环境修过类似的坑,就是这种递归轮询没控制好,用户切十几个页面能攒出几百个并发请求……
点赞 4
2026-02-12 19:26
夏侯治柯
拿去改改

export default {
data() {
return {
status: '',
pollTimer: null,
isPolling: true
};
},
mounted() {
this.poll();
},
beforeDestroy() {
this.isPolling = false;
if (this.pollTimer) {
clearTimeout(this.pollTimer);
this.pollTimer = null;
}
},
methods: {
async poll() {
// 页面已经销毁了就别继续了
if (!this.isPolling) return;

try {
const res = await axios.get('/api/poll', { timeout: 30000 });
this.status = res.data.status;

// 只有组件还活着才继续下一次轮询
if (this.isPolling) {
this.poll();
}
} catch (err) {
// 注意这里也要判断标志位,避免错误重试时组件已销毁
if (this.isPolling) {
this.pollTimer = setTimeout(() => this.poll(), 5000);
}
}
}
}
}


核心就是加个 isPolling 标志位,beforeDestroy 里关掉它。每次递归或者错误重试前都先判断一下组件还在不在。不然页面切走了,this 都没了你还去调 this.poll(),不报错谁报错。
点赞 5
2026-02-10 23:16