前端竞态问题总结

252 阅读2分钟

出现场景

  • 同一个tab来回切换
  • 根据输入单词查询接口
  • 两个按钮异步的形式操作同一份数据

解决

ui层面

防抖 全局loading

总结: 阻塞用户操作

防抖


function debounce(fun, delay) {
    return function (args) {
        let that = this
        let _args = args
        clearTimeout(fun.id)
        fun.id = setTimeout(function () {
            fun.call(that, _args)
        }, delay)
    }
}

对于输入框实时请求

闭包+对请求加id校验 +单实例


// 请求标记
let gobalReqID = 0

// 请求的函数
funtion query (keyword) {
  gobalReqID++
    let curReqID = gobalReqID
    return axios.post('/list', {
    keyword
  }).then(res => {
    // 对比闭包内的 curReqID 是否和 gobalReqID 一致
    if (gobalReqID === curReqID) {
        return res
    } else {
        return Promse.reject('无用的请求')
    }
  })
}

总结: 适合简单应用,可并发 ,无需要重复请求的诉求,多个组件同时初始化发请求搞不定

多实例 请求带着id,回来带着id,, tab多的话有性能问题

candelToken

其他补充:缓存请求也可以这里添加,原理同白名单类似


var CancelToken = axios.CancelToken;
var source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // 处理错误
  }
});

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');



//App.vue
export default {
  data() {
    return {
      //当前正在执行的请求列表
      requestList: [],
      //可重复请求的白名单列表
      sameRequestUrlWhiteList: []
    };
  },
  created() {
    //...其他操作
    this.setAxiosInterceptors();
  },
  methods: {
    setAxiosInterceptors() {
      this.$http.interceptors.request.use(request => {
        //给请求用单例模式配置一个独有的cancelToken
        request.cancelToken = new axios.CancelToken(function executor(c) {
          // 把cancel方法赋值到request的cancel属性上
          request.cancel = c;
        });
        //判断当前请求是否在可重复请求的白名单中,如果不在,则进入
        if (!this.sameRequestUrlWhiteList.includes(request.url)) {
          //对requestList做过滤,从requestList中移除重复请求的同时,执行cancel方法
          this.requestList = this.requestList.filter(item => {
            if (
              //通过url和method双重校验
              item.url === request.url &&
              item.method === request.method
            ) {
              //取消请求,同时返回false给filter
              item.cancel();
              return false;
            }
            //不做操作,返回false给filter
            return true;
          });
        }
        //将当前请求push到requestList中
        this.requestList.push(request);
        //...其他处理
        return request;
      });
      this.$http.interceptors.response.use(
        response => {
          //当接口响应之后,从 requestList 中移除
          this.requestList = this.requestList.filter(item => {
            if (
              item.url === response.config.url &&
              item.method === response.config.method
            ) {
              return false;
            }
            return true;
          });
          //...其他处理
          return response;
        },
        error => {
          //错误处理
        }
      );
    }
  }
}


cancel后 会报错 错误处理
error => {
  if (axios.isCancel(error)) {
    //处理被取消的错误
  } else {
    //处理正常错误
    this.$Message.error({
      content: "请求失败,请联系管理员"
    });
  }
  return Promise.reject(error.response);
}

总结: 可并发 

异步队列

/**
 * Using:
 * const queue = new RafAnimationQueue(defaultContext)
 * queue.add(firstAnimationFrameCallback) // firstAnimationFrameCallback will executed with defaultContext
 * queue.add(secondAnimationFrameCallback, customContext)
 * queue.delay() // skip just one frame
 * queue.clear() // clear animation queue
**/

export default class RafAnimationQueue {
  constructor(context) {
    this.context = context || undefined;
    this.clear();
  }
  add(callback, context) {
    if (callback instanceof Function) {
      this.queue.push(callback.bind(context || this.context));
      this.runQueue();
    }
    return this;
  }

  delay() {
    this.queue.push(undefined);
    this.runQueue();
    return this;
  }

  runQueue() {
    if (this.queueInProgress) {
      return;
    }
    this.queueInProgress = true;
    requestAnimationFrame(this.queueLoop.bind(this));
  }

  queueLoop() {
    const callback = this.queue.shift();
    callback instanceof Function && callback();

    if (!this.queue.length) {
      this.queueInProgress = false;
    } else {
      requestAnimationFrame(this.queueLoop.bind(this));
    }
  }
  // 加 await ,支持传入 异步函数 ,执行完后才执行下一个
  // async queueLoop() {
  //   const callback = this.queue.shift();
  //   callback instanceof Function && await callback(); 

  //   if (!this.queue.length) {
  //     this.queueInProgress = false;
  //   } else {
  //     requestAnimationFrame(this.queueLoop.bind(this));
  //   }
  // }

  clear() {
    this.queue = [];
  }
}

rxjs switchmap


var btn = document.querySelector('.js-query');
var inputStream = Rx.Observable.fromEvent(btn, 'click')
  .debounceTime(250) // 防抖,防止请求过于频繁
  .switchMap(url => Http.get(url)) 
  .subscribe(data => render(data));

资料