同一个请求发送多次,如何保证获取的是最后一次的结果
面试先给结论
同一个请求被连续发送多次时,要保证最终只采用最后一次请求的结果,常见做法有 3 种:
- 取消前一次请求,只保留最后一次
- 给每次请求打标记 / 序号,只处理最新那次响应
- 防抖 / 节流,从源头减少重复请求
实际项目里最稳妥的是:
请求取消 + 响应序号校验
这样即使前面的请求没成功取消,也不会污染最后的页面状态。
一、这个问题的本质是什么
这其实是典型的竞态问题(race condition)。
比如搜索框联想:
- 输入
a,发出请求 A - 很快又输入
ab,发出请求 B - 理论上页面应该显示
ab的结果
但如果网络情况是:
- 请求 B 先返回
- 请求 A 后返回
那如果你不做处理,A 的旧结果就会把 B 的新结果覆盖掉。
二、最常见方案 1:取消前一次请求
这是最直接的。
思路
每次发新请求前:
- 先把上一次请求取消
- 只保留当前这次请求
这样就不会出现“旧请求回来覆盖新请求”的问题。
axios 示例
旧版:CancelToken
let cancelFn = null function fetchData(params) { if (cancelFn) { cancelFn('取消上一次请求') } return axios.get('/api/search', { params, cancelToken: new axios.CancelToken((c) => { cancelFn = c }) }) }新版更推荐:AbortController
let controller = null async function fetchData(params) { if (controller) { controller.abort() } controller = new AbortController() try { const res = await fetch(`/api/search?q=${params}`, { signal: controller.signal }) const data = await res.json() return data } catch (err) { if (err.name === 'AbortError') { console.log('上一次请求已取消') return } throw err } }面试怎么说
如果是同一个接口被频繁触发,比如搜索建议,我会优先在发起新请求前取消上一次请求,只保留最后一次请求。这样可以减少无效请求,也能避免旧响应覆盖新数据。
三、最稳的方案 2:请求序号 / 时间戳校验
这是非常经典的方案,面试里一定要会说。
思路
每次发请求时给它一个递增 id:
- 请求 A:id = 1
- 请求 B:id = 2
- 请求 C:id = 3
响应回来时判断:
- 只有当前响应 id 等于“最新请求 id”,才更新页面
- 否则直接丢弃
这样即使旧请求没有被取消,它返回了也不会生效。
示例
let currentRequestId = 0 async function fetchData(keyword) { const requestId = ++currentRequestId const res = await fetch(`/api/search?q=${keyword}`) const data = await res.json() if (requestId !== currentRequestId) { // 说明这不是最后一次请求,丢弃结果 return } // 只有最后一次请求才能更新页面 render(data) }优点
- 实现简单
- 不依赖底层请求库是否支持取消
- 对任何异步请求都适用
面试表达
我会给每次请求加一个递增序号,响应回来后判断它是不是最新请求。如果不是最新的,就直接忽略。这样可以保证页面最终只使用最后一次请求的结果。
这句话很标准。
四、最佳实践:取消请求 + 序号校验一起用
这是最完整、最专业的回答。
因为现实里有两个问题:
- 取消请求不一定总能完全阻止后续逻辑
- 有些场景底层请求无法真正取消
所以更稳的方式是:
- 发新请求前先取消旧请求
- 响应回来后再做一次“是否最新”的判断
示例
let controller = null let latestRequestId = 0 async function fetchData(keyword) { if (controller) { controller.abort() } controller = new AbortController() const requestId = ++latestRequestId try { const res = await fetch(`/api/search?q=${keyword}`, { signal: controller.signal }) const data = await res.json() if (requestId !== latestRequestId) return render(data) } catch (err) { if (err.name === 'AbortError') return throw err } }为什么这套最稳
- 取消请求:减少无效网络开销
- 序号校验:防止旧结果误更新页面
五、源头治理:防抖
如果这个场景是输入框实时搜索,除了保证“只取最后一次结果”,还应该考虑:
减少根本没必要发出的请求
这时就用防抖。
示例
function debounce(fn, delay = 300) { let timer = null return function (...args) { clearTimeout(timer) timer = setTimeout(() => { fn.apply(this, args) }, delay) } } const search = debounce((keyword) => { fetchData(keyword) }, 300)适用场景
- 搜索建议
- 输入联想
- 筛选条件变化后请求列表
但要注意
防抖只是减少请求次数,不等于一定能解决竞态问题。
所以更严谨的说法是:
防抖用来减少重复请求,取消请求和响应校验用来保证最终结果正确。
六、Vue / React 里常见怎么做
Vue 里
常见于:
watch(inputValue, ...)- 输入框搜索
- 列表筛选
思路:
- 监听关键字变化
- 防抖触发请求
- 每次请求前取消前一次
- 返回后校验是否最新
React 里
常见于:
useEffect- 搜索输入
- 条件筛选
会配合:
AbortController- 闭包里的 requestId
- 清理函数里取消请求
七、如果面试官问“只用时间戳可以吗”
可以。
比如每次请求记录一个时间戳:
let latestTime = 0 async function fetchData() { const time = Date.now() latestTime = time const res = await fetch('/api/data') const data = await res.json() if (time !== latestTime) return render(data) }但是更推荐用:
- 递增
requestId
因为更明确,也不会受时间精度影响。
八、如果是 Promise,怎么保证只取最后一次
可以封装一个takeLatest思路。
示例
function createTakeLatest(asyncFn) { let latestId = 0 return async function (...args) { const id = ++latestId const result = await asyncFn(...args) if (id !== latestId) return return result } }使用:
const fetchLatest = createTakeLatest(async (keyword) => { const res = await fetch(`/api/search?q=${keyword}`) return res.json() }) fetchLatest('a').then(res => { if (res) render(res) })九、面试标准回答模板
你可以直接这样答:
这个问题本质上是多个异步请求并发导致的竞态问题。
比如同一个接口连续请求多次时,前一次请求可能比后一次更晚返回,从而把最新结果覆盖掉。
为了解决这个问题,我一般会有三层处理:
- 发新请求前取消上一次请求,减少无效网络开销;
- 给每次请求加递增序号或时间戳,响应回来时判断它是不是最新请求,只有最新请求才能更新页面;
- 如果是输入联想这类高频触发场景,再配合防抖,从源头减少重复请求。
实际项目里我更推荐“取消请求 + 请求序号校验”一起用,这样即使旧请求没有成功取消,也能保证最终页面只展示最后一次请求的结果。
十、30 秒短答版
同一个请求连续发送多次时,我会把它当作竞态问题处理。
常见做法是:发新请求前取消旧请求,并且给每次请求加一个 requestId,响应回来时只有 id 等于最新值才更新页面。
如果是搜索框这类场景,还会再加防抖减少无效请求。
这样可以保证最终拿到并展示的是最后一次请求的结果。
十一、一句话总结
保证只取最后一次结果,核心不是让前面的请求“不返回”,而是让前面的结果“即使返回也无效”。
