Skip to content

异步请求竞态问题

通过记录全局的唯一id来解决

TIP

核心思路: 记录一个全局id,每次请求触发时id自增,并且将id作为响应数据返回。 在请求完成处理响应数据时,判断全局id跟响应数据返回的id,如果两个值相等则代表此次请求是最新的请求,处理响应数据即可。

tips: 本质上这种方法是放弃了旧的请求响应数据,只处理最新的请求响应数据。

实现

js
let index = 0
const request = (msg, id) => {
  return new Promise(resolve => {
    let current = ++index
    setTimeout(() => {
      resolve({
        errno: 0,
        msg,
        current,
      })
    }, id * 1000)
  })
}

const search = async (params, id) => {
  const res = await request(params, id)
  if (res.current === index) {
    console.log(res, 'request success')
  }
}

// test case
search('first req', 0.5)
search('second req', 2)
search('three req', 0.1) // { errno: 0, msg: 'three req', current: 3 } request success

使用Promise和xhr.abort(): Axios中如何解决异步请求竞态问题

TIP

核心思路: 利用promise和XMLHttpRequest的abort方法解决xhr异步请求的竞态问题(本文示例用计时器模拟xhr异步事件)。

每一个异步请求都会返回一个 pending 的 promise ,因此可以在连续的请求中将上一个请求的promise reject, 同时执行XHR对象的abort方法,确保响应的请求是最新的。

那要如何reject上一个promise呢? 可以通过插入一个新的promise(下文称cancelPromise),在发起请求的promise中(下文称xhrPromise)来监听cancelPromise的状态(cancelPromise.then)。

同时我们将cancelPromise的控制权(resolve函数)交由到外层,可以由开发者决定何时调用(即手动调用cancel函数的时候)。

那么当cancelPromise的状态被改变时(调用resolve), 在xhrPromise内监听cancelPromise状态的回调函数就会执行(cancelPromise.then), 因此就能reject xhrPromise并且执行取消请求(xhr.abort)

tips: axios中取消xhr请求相关实现

Details
js
// 错误消息类
class CanceledError {
  constructor(message) {
    this.message = message
  }
}
// 取消请求类
class CancelToken {
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new TypeError('executor must be a function')
    }
    
    // 记录cancelPromise的resolve,供外层调用可用来改变cancelPromise的状态
    let resolvePromise
    // cancelPromise
    this.promise = new Promise(resolve => {
      resolvePromise = resolve
    })

    let token = this

    // 执行executor,并将cancel函数作为参数传入,则外部可以获取到cancel函数(函数内部调用resolvePromise,可改变cancelPromise的状态)
    executor(function cancel(message) {
      if (token.reason) return
      token.reason = new CanceledError(message)
      resolvePromise(token.reason)
    })
  }

  static source() {
    let cancel
    const token = new CancelToken(c => {
      cancel = c
    })
    return {
      token,
      cancel
    }
  }
}
// 异步请求包装
function xhrAdapter(url, cancelToken) {
  return new Promise((resolve, reject) => {
    // 模拟异步请求
    let timeId = setTimeout(() => {
      resolve(`响应数据: ${url}`)
    }, url)

    if (cancelToken) {
      // 监听cancelPromise回调,回调里reject本次promise,同时取消异步请求;
      // tips: 开发者手动调用cancel函数时会执行resolvePromise,然后此处回调执行。
      cancelToken.promise.then(reason => {
        clearTimeout(timeId)
        reject({
          reason,
          isCancel: true,
        })
      })
    }
  })
}


// test case
// 1. 使用new CancelToken()
let cancel
let reqId = 0
function run(id) {
  if (cancel) {
    cancel(`取消第${++reqId}个请求`)
  }
  xhrAdapter(id * 1000, new CancelToken(c => {
    cancel = c
  })).then(res => {
    console.log(res, 'request success')
  }).catch(err => {
    // console.log(err, 'request catch')
    if (err.isCancel) {
      console.log(err.reason.message, 'request canceled')
    } else {
      console.log(err, 'request error')
    }
  })
}

// 2. 使用CancelToken.source()
let source
let reqId = 0
function run(id) {
  if (source) {
    source.cancel(`取消第${++reqId}个请求`)
  }
  source = CancelToken.source()
  xhrAdapter(id * 1000, source.token).then(res => {
    console.log(res, 'request success')
  }).catch(err => {
    if (err.isCancel) {
      console.log(err.reason.message, 'request canceled')
    } else {
      console.log(err, 'request error')
    }
  })
}

// 测试同时发起多个请求,仅响应最新的请求
run(1.5)
run(2)
run(1)

fetch和AbortController

TIP

AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。

fetch和AbortController实现取消请求的实现及相关概念,可以查看文档

tips:v0.22.0 开始,Axios 支持以 fetch API 方式—— AbortController 取消请求; 并且xhr相关的CancelToken也从v0.22.0开始被弃用。