如何取消axios请求

Posted 老张在线敲代码

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何取消axios请求相关的知识,希望对你有一定的参考价值。

使用 CancelToken.source 工厂方法创建 cancel token

// CancelToken是一个构造函数,用于创建一个cancelToken实例对象
// cancelToken实例对象包含了一个promise属性,值为可以触发取消请求的一个promise
const CancelToken = axios.CancelToken;
// 执行source()得到的是一个包含了cancelToken对象和一个取消函数cancel()的对象
// 即 source = {token: cancelToken对象, cancel: 取消函数}
const source = CancelToken.source();

// 在请求的配置中配置cancelToken,那么这个请求就有了可以取消请求的功能
axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
     // 处理错误
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

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

通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    // 把cancel函数传递给外面,使得外面能控制执行取消请求
    cancel = c;
  })
});

// cancel the request
cancel();

看起来有些晕,毕竟不知道里面是怎么运作的,稍后通过源码解析。这里简单解释就是,在请求中配置cancelToken这个属性,是为了使得请求具有可以取消的功能;cancelToken属性的值是一个CancelToken实例对象,在它的executor函数中提取出cancel函数,执行这个cancel函数来取消请求。

我的实例

点击标签,执行getCourse函数。点击某个标签时,先取消之前的请求(如果之前的请求已完成,取消请求不会有任何操作)。效果是,页面显示的总是最后点击的标签对应的结果。

分两步:
第一步:在get请求中配置cancelToken属性,开启取消请求的功能,且在其属性值中将cancel函数赋cancelRequest,使得外部可以调用cancel函数来取消请求;
第二步:在执行请求前,先取消前一次的请求。

import axios from 'axios'
export default {
    data() {
        return {
            cancelRequest: null // 初始时没有请求需要取消,设为null
        }
    },
    methods: {
        // 点击标签后发送请求的函数
        getCourse() {
            const that = this
            // 2. 准备执行新的请求前,先将前一个请求取消
            // 如果前一个请求执行完了,执行取消请求不会有其他操作
            if (typeof that.cancelRequest === 'function') {
                that.cancelRequest()
            }
           // 这里配置请求的参数,略
           let params = {}
           // 发送请求
           axios.get('/api/app/course',{
               params: params,
               cancelToken: new CancelToken(function executor(c) {
                    // 1. cancel函数赋值给cancelRequest属性
                    // 从而可以通过cancelRequest执行取消请求的操作
                    that.cancelRequest = c
              }) 
           })
        }
    }
}

一般API都会统一封装,所以,可以将请求封装起来

API

// /api/modules/course.js
// _this为vue组件实例对象
export function getCourseReq(params, _this) {
    return axios.get('/api/app/course',{
           params: params,
           cancelToken: new CancelToken(function executor(c) {
                // 1. cancel函数赋值给cancelRequest属性
                // 从而可以通过cancelRequest执行取消请求的操作
                _this.cancelRequest = c
          }) 
       })
       .then(res => {})
       .catch(err => {})
}

组件

import { getCourseReq } from '@/apis/modules/course'

methods: {
    getCourse() {
        // 2. 准备执行新的请求前,先将前一个请求取消
        // 如果前一个请求执行完了,执行取消请求不会有其他操作
        if (typeof this.cancelRequest === 'function') {
            this.cancelRequest()
        }
       // 这里配置请求的参数,略
       let params = {}
       // 发送请求
       getCourseReq(params, this)
           .then(res => {})
           .catch(err => {})
    }
}

遇到的问题

一开始按照上述方法写好,但请求死活没有取消。一遍遍核对了变量名,调试输出信息,啥都对,就是没法取消。折腾半天,想起前人配置的axios的interceptor里对每个请求配置了cancelToken,目的是为了去掉重复的请求,但取消重复请求的代码段被注释掉了。把cancelToken注释掉之后,终于雨过天晴。

axios.interceptors.request.use(
  config => {
    const request =
      JSON.stringify(config.url) +
      JSON.stringify(config.method) +
      JSON.stringify(config.data || '')
      // 这里配置了cancelToken属性,覆盖了原请求中的cancelToken
     config.cancelToken = new CancelToken(cancel => {
       sources[request] = cancel
     })
    // if (requestList.includes(request)) {
    //   sources[request]('取消重复请求')
    // } else {
    requestList.push(request)
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

由于interceptor会在请求发送前做一些配置处理,这里把原请求中的cancelToken覆盖了,那么即使原请求中执行原cancelToken的cancel函数,由于cancelToken对象不同了,取消操作也就无效了。后面看了源码可以更明白为什么无效。

源码解析

根据前面的步骤,依次来看看各个源码是怎样。

首先,我们为请求配置cancelToken属性,目的是使得请求具有能取消请求的功能,它的值是CancelToken实例对象,那么CancelToken是什么呢?

// axios/lib/cancel/CancelToken.js

'use strict';

var Cancel = require('./Cancel');

function CancelToken(executor) {
    if (typeof executor !== 'function') {
        throw new TypeError('executor must be a function.');
    }
    /**
    * 定义一个将来能执行取消请求的promise对象,当这个promise的状态为完成时(fullfilled),
    * 就会触发取消请求的操作(执行then函数)。而执行resolve就能将promise的状态置为完成状态。
    * 这里把resolve赋值给resolvePromise,就是为了在这个promise外能执行resolve而改变这个promise的状态
    * 注意这个promise对象被赋值给CancelToken实例的属性promise,将来定义then函数就是通过这个属性得到promise
    */
    var resolvePromise;
    this.promise = new Promise(function promiseExecutor(resolve) {
        resolvePromise = resolve;
    });
    /**
    * 将CancelToken实例赋值给token
    * 执行executor函数,将cancel方法传入executor,
    * cancel方法可调用resolvePromise方法,即触发取消请求的操作
    */
    var token = this;
    executor(function cancel(message) {
        if (token.reason) {
            // 取消已响应 返回
            return;
        }
        token.reason = new Cancel(message);
        // 这里执行的就是promise的resolve方法,改变状态
        resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
    if (this.reason) {
        throw this.reason;
    }
};

// 这里可以看清楚source函数的真面目
CancelToken.source = function source() {
    var cancel;
    var token = new CancelToken(function executor(c) {
        // c 就是CancelToken中给executor传入的cancel方法
        cancel = c;
    });
    return {
        token: token,
        cancel: cancel
    };
};

module.exports = CancelToken;
CancelToken
CancelToken是一个构造函数,通过new CancelToken()得到的是一个实例对象,它只有一个属性promise, 它的值是一个能触发取消请求的Promise对象。

token = new CancelToken(executor function) ===> { promise: Promise对象 }

执行CancelToken函数做了两件事:

创建一个Promise对象,且将这个对象赋值给promise属性,其resolve参数被暴露出来以备外部引用。
执行executor函数,将内部定义的cancel函数作为参数传递给executor
// 源码相当于:
var token = this;
var cancel = function (message) {
    if (token.reason) {
        // 取消已响应 返回
        return;
    }
    token.reason = new Cancel(message);
    // 这里执行的就是promise的resolve方法,改变状态
    resolvePromise(token.reason);
}
executor(cancel);
所以执行

let cancel
token = new CancelToken(function executor(c) {
    cancel = c;
});
得到结果是:

token值为{promise: Promise对象}
executor函数被执行,即cancel = c执行,因此变量cancel被赋值了,值就是CanelToken内部的那个cancel函数。
题外话,发现Promise其实也是传入executor,跟执行new Promise(executor)是一样的

CancelToken.source
CancelToken.source是一个函数,通过源码可以看到,执行const source = CancelToken.source(),得到的是一个对象:

return {
    token: token,
    cancel: cancel
};
包含一个token对象,即CancelToken实例对象,和一个cancel函数。因此CancelToken.source()函数的作用是生成token对象和取得cancel函数。token对象是用于配置给axios请求中的cancelToken属性,cancel函数是将来触发取消请求的函数。

如何执行取消请求的
万事俱备,就差如何执行取消请求了。这也是我一开始最疑惑的地方,一起来看看吧。

// axios/lib/adapters/xhr.js

// 创建XHR对象
var request = new XMLHttpRequest()
// 模拟当前ajax请求
request.open(config.method.toUpperCase(), buildURL(config.url, config.params, config.paramsSerializer), true)

// 定义取消请求promise对象的then函数
if (config.cancelToken) { // 如果配置了cancelToken属性
    // 当promise为完成态时,这个then函数执行,即执行取消请求
    config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
            return;
        }
        // 取消ajax请求
        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
    });
}

这是最终执行ajax请求的地方。

我们前面配置了cancelToken = new CancelToken(executor),得到对象{promise: 取消请求的Promise对象},因此上面cancelToken.promise就是这个promise。

我们知道,Promise的then函数是在Promise对象处于fullfilled时执行,这就是暗中的关联。执行cancel函数,会将Promise的状态置为fullfilled,这里定义的then函数就会执行,从而取消请求。

拦截器问题

之前遇到的问题,拦截器中配置的cancelToken覆盖了请求中的cancelToken,导致请求失效。原因是,拦截器重新定义后,cancelToken属性值变成了新的token,cancelToken.promise也就变成了新的promise。then函数是在拦截器之后、真正发送请求之前定义的,因此这时定义的then函数,是对应这个新的Promise对象的,于是原来token的promise并没有then函数,那么执行原cancel函数把原Promise对象置为了fullfilled状态,但没有相应的then函数执行,有人发信号,没人执行,取消请求无效。

axios流程:定义请求 -> 请求拦截器 -> 发送请求 -> 响应拦截器 -> 接收响应

链接:https://www.jianshu.com/p/c9e05ac3a2a1

以上是关于如何取消axios请求的主要内容,如果未能解决你的问题,请参考以下文章

VSCode自定义代码片段14——Vue的axios网络请求封装

Axios 如何取消已发送的请求?

如何取消axios请求

前端进阶使用fetch/axios时, 如何取消http请求

使用 axios 和 vue.js 取消先前的请求

前端热门技术axios详细讲解——取消异步请求