如何避免异步请求导致数据错乱的问题

361 阅读2分钟

背景:在需求开发中,切换tab组件,查询对应tab值所对应的列表;当时直接根据请求参数params调后端接口获取数据,在网络条件差的情况下,请求顺序不能保证就极其容易复现;

eg: Coding中代码仓库合并请求查看全部和某一个人的提交,就很容易复现

image.png

异步请求导致数据错乱的主要原因是请求响应顺序不确定

// 示例:快速切换页码
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();
  }
}