背景:在需求开发中,切换tab组件,查询对应tab值所对应的列表;当时直接根据请求参数params调后端接口获取数据,在网络条件差的情况下,请求顺序不能保证就极其容易复现;
eg: Coding中代码仓库合并请求查看全部和某一个人的提交,就很容易复现
异步请求导致数据错乱的主要原因是请求响应顺序不确定
// 示例:快速切换页码
async function fetchList(page) {
try {
// 第二页的请求可能比第一页更快返回
const res = await axios.get(`/api/list?page=${page}`);
this.list = res.data; // 这里可能用较旧的数据覆盖新数据
} catch (error) {
console.error(error);
}
}
// 用户操作
fetchList(1); // 请求第1页
fetchList(2); // 快速切换到第2页
下面介绍几种避免异步请求导致数据错乱的解决方案:
1. 请求取消
使用 axios 的 CancelToken 机制:
export default {
data() {
return {
cancelToken: null,
loading: false,
list: []
}
},
methods: {
async fetchData(params) {
// 取消之前的请求
if (this.cancelToken) {
this.cancelToken.cancel('新请求已发起');
}
// 创建新的 CancelToken
this.cancelToken = axios.CancelToken.source();
try {
this.loading = true;
const res = await axios.get('/api/data', {
params,
cancelToken: this.cancelToken.token
});
this.list = res.data;
} catch (error) {
if (axios.isCancel(error)) {
console.log('请求已取消:', error.message);
} else {
console.error(error);
}
} finally {
this.loading = false;
}
}
},
// 组件销毁时取消未完成的请求
beforeDestroy() {
if (this.cancelToken) {
this.cancelToken.cancel('组件已销毁');
}
}
}
2. 请求标记
使用请求序号或时间戳、请求参数标记:
export default {
data() {
return {
requestId: 0,
list: []
}
},
methods: {
async fetchData(params) {
const currentRequestId = ++this.requestId;
// 通过请求参数判断
// const params = Clone(this.params)
try {
const res = await axios.get('/api/data', { params });
// 只有当前请求的结果才更新数据
if (currentRequestId === this.requestId) {
this.list = res.data;
}
// 只有当前请求参数的结果才更新数据
// isEqual 用于比较两个对象是否相等的工具函数
// if (isEqual(params,this.params)) {
// this.list = res.data;
// }
} catch (error) {
console.error(error);
}
}
}
}
3. 防抖和节流
对频繁触发的请求进行控制:
export default {
data() {
return {
list: []
}
},
created() {
// 创建防抖版本的请求方法
this.debouncedFetch = _.debounce(this.fetchData, 300);
},
methods: {
// 原始请求方法
async fetchData(params) {
try {
const res = await axios.get('/api/data', { params });
this.list = res.data;
} catch (error) {
console.error(error);
}
},
// 搜索输入时调用防抖版本
handleSearch(query) {
this.debouncedFetch({ query });
}
},
beforeDestroy() {
// 取消未执行的防抖函数
this.debouncedFetch.cancel();
}
}
4. 状态锁
防止重复请求:
export default {
data() {
return {
loading: false,
list: []
}
},
methods: {
async fetchData(params) {
// 如果正在加载中,直接返回
if (this.loading) return;
this.loading = true;
try {
const res = await axios.get('/api/data', { params });
this.list = res.data;
} catch (error) {
console.error(error);
} finally {
this.loading = false;
}
}
}
}
5. 请求队列
控制请求顺序:
export default {
data() {
return {
requestQueue: [],
list: []
}
},
methods: {
async fetchData(params) {
const request = this.createRequest(params);
this.requestQueue.push(request);
// 等待之前的请求完成
while (this.requestQueue[0] !== request) {
await new Promise(resolve => setTimeout(resolve, 100));
}
try {
const res = await axios.get('/api/data', { params });
this.list = res.data;
} finally {
// 从队列中移除当前请求
this.requestQueue.shift();
}
},
createRequest(params) {
return {
params,
timestamp: Date.now()
};
}
}
}
6. 组合使用
在实际项目中,可以组合使用多种方案:
export default {
data() {
return {
cancelToken: null,
requestId: 0,
loading: false,
list: []
}
},
created() {
this.debouncedFetch = _.debounce(this.fetchData, 300);
},
methods: {
async fetchData(params) {
// 状态锁检查
if (this.loading) return;
// 取消之前的请求
if (this.cancelToken) {
this.cancelToken.cancel('新请求已发起');
}
// 创建新的 CancelToken
this.cancelToken = axios.CancelToken.source();
// 请求标记
const currentRequestId = ++this.requestId;
this.loading = true;
try {
const res = await axios.get('/api/data', {
params,
cancelToken: this.cancelToken.token
});
// 检查请求标记
if (currentRequestId === this.requestId) {
this.list = res.data;
}
} catch (error) {
if (!axios.isCancel(error)) {
console.error(error);
}
} finally {
this.loading = false;
}
},
// 搜索处理
handleSearch(query) {
this.debouncedFetch({ query });
}
},
beforeDestroy() {
// 清理工作
if (this.cancelToken) {
this.cancelToken.cancel('组件已销毁');
}
this.debouncedFetch.cancel();
}
}