likes
comments
collection
share

axios取消功能的设计与实现

作者站长头像
站长
· 阅读数 22

需求分析

有些场景下,我们希望能主动取消请求,比如常见的搜索框案例,在用户输入过程中,搜索框的内容也在不断变化,正常情况每次变化我们都应该向服务端发送一次请求。

但是当用户输入过快的时候,我们不希望每次变化请求都发出去,通常一个解决方案是前端用 debounce 的方案,比如延时 200ms 发送请求。这样当用户连续输入的字符,只要输入间隔小于 200ms,前面输入的字符都不会发请求。

但是还有一种极端情况是后端接口很慢,比如超过 1s 才能响应,这个时候即使做了 200ms 的 debounce,但是在我慢慢输入(每个输入间隔超过 200ms)的情况下,在前面的请求没有响应前,也有可能发出去多个请求。

因为接口的响应时长是不定的,如果先发出去的请求响应时长比后发出去的请求要久一些,后请求的响应先回来,先请求的响应后回来,就会出现前面请求响应结果覆盖后面请求响应结果的情况,那么就乱了。

因此在这个场景下,我们除了做 debounce,还希望后面的请求发出去的时候,如果前面的请求还没有响应,我们可以把前面的请求取消

从 axios 的取消接口设计层面,我们希望做如下的设计:

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

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

// 取消请求,参数是请求原因
source.cancel('Operation canceled by the user.');

我们给 axios 添加一个 CancelToken 的对象,它有一个 source 方法可以返回一个 source 对象,source.token 是在每次请求的时候传给配置对象中的 cancelToken 属性,然后在请求发出去之后,我们可以通过 source.cancel 方法取消请求。

我们还支持另一种方式的调用:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});

// 取消请求
cancel('Operation canceled by the user.');

axios.CancelToken 是一个类,我们直接把它实例化的对象传给请求配置中的 cancelToken 属性,CancelToken 的构造函数参数支持传入一个 executor 方法,该方法的参数是一个取消函数 c,我们可以在 executor 方法执行的内部拿到这个取消函数 c,赋值给我们外部定义的 cancel 变量,之后我们可以通过调用这个 cancel 方法来取消请求。

异步分离的设计方案

通过需求分析,我们知道想要实现取消某次请求,我们需要为该请求配置一个 cancelToken,然后在外部调用一个 cancel 方法。

请求的发送是一个异步过程,最终会执行 xhr.send 方法,xhr 对象提供了 abort (opens new window)方法,可以把请求取消。因为我们在外部是碰不到 xhr 对象的,所以我们想在执行 cancel 的时候,去执行 xhr.abort 方法。

现在就相当于我们在 xhr 异步请求过程中,插入一段代码,当我们在外部执行 cancel 函数的时候,会驱动这段代码的执行,然后执行 xhr.abort 方法取消请求。

我们可以利用 Promise 实现异步分离,也就是在 cancelToken 中保存一个 pending 状态的 Promise 对象,然后当我们执行 cancel 方法的时候,能够访问到这个 Promise 对象,把它从 pending 状态变成 resolved 状态,这样我们就可以在 then 函数中去实现取消请求的逻辑,类似如下的代码:

const request = new XMLHttpRequest()
// ...

if (cancelToken) {
  cancelToken.promise
    .then(reason => {
      request.abort()
      reject(reason)
    })
}
request.send(data)

CancelToken 类实现

单独创建 cancel 目录来管理取消相关的代码,在 cancel 目录下创建 CancelToken.ts 文件:

export default class CancelToken {
  promise: Promise<string>
  reason?: string

  constructor(executor: CancelExecutor) {
    let resolvePromise: ResolvePromise
    this.promise = new Promise<string>(resolve => {
      resolvePromise = resolve
    })

    executor(message => {
      if (this.reason) {
        return
      }
      this.reason = message
      resolvePromise(this.reason)
    })
  }
}

在 CancelToken 构造函数内部,实例化一个 pending 状态的 Promise 对象,然后用一个 resolvePromise 变量指向 resolve 函数。接着执行 executor 函数,传入一个 cancel 函数,在 cancel 函数内部,会调用 resolvePromise 把 Promise 对象从 pending 状态变为 resolved 状态。

接着我们在 xhr.ts 中插入一段取消请求的逻辑,core/xhr.ts

const { /*....*/ cancelToken } = config

if (cancelToken) {
  cancelToken.promise.then(reason => {
    request.abort()
    reject(reason)
  })
}

这里有个非常巧妙的设计,如果我想触发promise.then里面的回调函数,必须要执行promise里面的resolve方法,但是这个触发必须是由用户自己调用cancel方法的时候触发,所以就有了下面的这段代码:

let resolvePromise
this.promise = new Promise<string>(resolve => {
   resolvePromise = resolve
})

我们用一个变量resolvePromise指向了promiseresolve方法,在实例化CancelToken时执行构造函数,构造函数里面又执行了用户传入的executor方法,这个方法的参数是一个函数,这个函数就是cancel方法,我们把这个cancel方法传递给了外部变量cancel

let cancel
axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});

这样外部变量cancel就指向了resolvePromise方法,执行这个方法就是执行promiseresolve方法,进而触发then里面的回到函数,取消本次请求。

executor(message => {
  if (this.reason) {
    return
  }
  this.reason = new Cancel(message)
  resolvePromise(this.reason)
})

这样就满足了第二种使用方式,接着我们要实现第一种使用方式,给 CancelToken 扩展静态接口。

CancelToken 扩展静态接口

我们来看下第一种使用方式:

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

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

// 取消请求,参数是请求原因
source.cancel('Operation canceled by the user.');

CancelToken就是CancelToken类,它有一个静态方法source,这个方法返回一个实例token以及一个cancel方法。

下面来给CancelToken类添加一个静态方法:

export default class CancelToken {
  // ...

  static source(): CancelTokenSource {
    let cancel!: Canceler
    const token = new CancelToken(c => {
      cancel = c
    })
    return {
      cancel,
      token
    }
  }
}

source 的静态方法很简单,定义一个 cancel 变量实例化一个 CancelToken 类型的对象,然后在 executor 函数中,把 cancel 指向参数 c 这个取消函数。这样就满足了我们第一种使用方式。