五分钟!让你彻底搞懂axios的请求取消原理!附源码分析

5,995 阅读7分钟

1 前言

本文主要介绍axios是如何设计取消请求的。

如果您对axios请求整体流程还不太熟悉,建议先大致浏览一下该篇文章:

用了这么久的axios,没想到源码居然这么简单!

2 取消请求的思路

取消请求主要分为下面三种思路:

  1. 发送请求前取消
  2. 发送请求过程中取消
  3. 请求接收后取消

其中第一点和第三点比较好实现,只要在请求前和请求后判断是否取消了就行,如果取消,就抛出异常。

第二点Axios使用类似观察者的模式,在请求前先注册好请求取消事件(abort),一旦在请求过程中用户取消了请求,就会调用abort,实现请求的取消。

3 取消请求的流程

axios提供两种方式给用户,用来取消请求,分别是CancelTokenAbortController形式,我们先来看CancelToken的设计和实现,使用AbortController的原理其实与CancelToken是一样的,只不过会更加简便。

3.1 CancelToken

CancelToken的静态方法CancelToken.source返回了两个东西:分别是tokencancel方法,先记住这两个东西的用途,后面会在源码中分析。

CancelToken.png

3.2 取消请求的流程

请求取消流程.png

可以看到,流程里,axios分别在请求前请求中请求后进行了是否取消的判断。

这也就意味着,当我们在请求拦截器中调用cancel方法,axios会识别到,抛出异常。

如果我们在返回拦截器中调用cancel方法,axios就识别不到了,当然如果真要这么用,也不是不行,可以自己去判断token的状态,进行处理。

4 CancelToken.js

接下来我们看看CancelToken是怎么实现的吧:

// 用来取消请求的类
class CancelToken {
  // 构造函数接收一个executor函数,
  // 提前说明:该函数的回调参数为一个cancel函数
  // 在下面可以看到
  // 执行了该cancel函数之后,就说明这个Token被取消了
  constructor(executor) {
    // executor必须为方法
    if (typeof executor !== 'function') {
      throw new TypeError('executor must be a function.');
    }

    let resolvePromise;

    // 定义一个promise,
    // 将promise的resolve取出来
    // 这样就可以在其他地方更改promise的状态了
    this.promise = new Promise(function promiseExecutor(resolve) {
      resolvePromise = resolve;
    });

    const token = this;

    // 如果取消了,执行监听函数
    // 也就是通过token.subscripbe收集的订阅函数
    this.promise.then((cancel) => {
      if (!token._listeners) return;

      let i = token._listeners.length;

      while (i-- > 0) {
        token._listeners[i](cancel);
      }
      token._listeners = null;
    });

    // 修改默认的promise.then方法,
    this.promise.then = (onfulfilled) => {
      let _resolve;

      // 新建一个promise,
      // 订阅函数
      const promise = new Promise((resolve) => {
        token.subscribe(resolve);
        _resolve = resolve;
      }).then(onfulfilled);

      // 增加cancel函数
      // 取消订阅
      promise.cancel = function reject() {
        token.unsubscribe(_resolve);
      };

      // 返回的是新的promise
      return promise;
    };

    // 关键
    // 执行传入的executor函数,
    // 回调参数是一个cancel函数
    // 该函数可以用来取消
    executor(function cancel(message, config, request) {
      // 用token.reason判断是否取消
      if (token.reason) {
        // 说明已经取消过了
        return;
      }

      // 新建一个异常,赋值给reason
      token.reason = new CanceledError(message, config, request);
      // 执行后会改变promise的状态为fulfilled,
      // 这之后会执行this.promise.then()
      // 注意resolve传入的是CanceledError
      resolvePromise(token.reason);
      
    });
  }

  // 如果已经取消过了,抛出异常
  throwIfRequested() {
    if (this.reason) {
      throw this.reason;
    }
  }

  // 添加取消事件订阅
  subscribe(listener) {
    // 如果已经取消过了,执行listener方法
    if (this.reason) {
      listener(this.reason);
      return;
    }
    // 否则将该函数加到_listeners数组中
    if (this._listeners) {
      this._listeners.push(listener);
    } else {
      this._listeners = [listener];
    }
  }

  // 移除取消事件订阅
  unsubscribe(listener) {
    if (!this._listeners) {
      return;
    }
    const index = this._listeners.indexOf(listener);
    if (index !== -1) {
      this._listeners.splice(index, 1);
    }
  }

  // 类的静态方法
  // 返回token和cancel方法
  // 这样就可以通过执行cancel()改变token的状态
  static source() {
    let cancel;
    const token = new CancelToken(function executor(c) {
      cancel = c;
    });
    return {
      token,
      cancel,
    };
  }
}

export default CancelToken;

5 请求取消的整体流程

以下代码是我整合了axios请求中多个文件的关键部分代码,有点伪代码的意思,这样看起来比较连贯,好理解。

request (configOrUrl, config) {

    // 合并配置
    config = mergeConfig(this.defaults, config);
    
    // 合并请求头信息 
    config.headers = AxiosHeaders.concat(contextHeaders, headers);
    
    // 请求拦截器链
    const requestInterceptorChain = [];
    this.interceptors.request.forEach(function unshiftRequestInterceptors (interceptor) {
      requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
    });

    // 响应拦截器链
    const responseInterceptorChain = [];
    this.interceptors.response.forEach(function pushResponseInterceptors (interceptor) {
      responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
    });
    
    // 执行请求拦截器方法
    doRequestInterceptorChain()
    
    // 判断config.cancelToken.reason,也就是是否被取消
    throwIfCancellationRequested(config);
    
    // 请求体
    let requestData = config.data;
    
    // 创建一个xhr实例
    let request = new XMLHttpRequest();
    
    // open xhr
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    
    // 请求状态变更处理函数
    request.onreadystatechange = function handleLoad () {
        //.....
    };
    
    // 订阅取消事件
    if (config.cancelToken || config.signal) {
      
      // 当token取消了,会执行该函数,也就是abort
      onCanceled = cancel => {
        if (!request) {
          return;
        }
        reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
        request.abort();
        request = null;
      };

      // 此处是用cancelToken的订阅取消事件
      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      // 此处是用signal,也就是AbortConrtroller订阅取消事件
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }
    
    // 真正发送请求
    request.send(requestData || null);
    
    // 再次判断config.cancelToken.reason,也就是是否被取消
    throwIfCancellationRequested(config);

    // 执行返回拦截器
    doResponseInterceptorChain()
    
}

6 AbrotController

现在再回头,让我们看看官网提供的AbortControllerCancelToken形式的使用方式:

const controller = new AbortController();

axios.get('/foo/bar', {
   signal: controller.signal
}).then(function(response) {
   //...
});
// 取消请求
controller.abort()
const CancelToken = axios.CancelToken;
const 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.');

对比一下两者:

controller指代AbortController

source指代CancelToken

AbortControllerCancelToken
传入方式controller.signalsource.token
判断是否取消controller.signal.abortedsource.token.resaon
订阅取消事件controller.signal.addEventListener('abort', onCanceled)source.token.subscribe(onCanceled)
用户取消controller.abort()source.cancel()

对比后我们发现,这两者不能说是很像,只能说是一模一样好吧!

我的理解是:CancelToken其实就是作者自己实现的一版AbortController,虽然细节不太一样,但是思路其实是一样的。

7 异常类

最后,再补充介绍一下axios中的异常类:AxiosErrorCanceledError

通过名称可以猜到,AxiosError是作者自定义专门给axios项目使用的异常类,而CanceledErrorAxiosError里更具体的一种异常,等于是AxiosError的子类。

7.1 AxiosError.js

// AxiosError的构造函数
function AxiosError(message, code, config, request, response) {
  Error.call(this);

  // 只有谷歌的V8引擎有该api
  if (Error.captureStackTrace) {
    // 将捕获的异常堆栈信息写入到this的.stack属性中
    Error.captureStackTrace(this, this.constructor);
  } else {
    // 其他浏览器的写法
    this.stack = new Error().stack;
  }

  this.message = message;
  this.name = 'AxiosError';
  code && (this.code = code);
  config && (this.config = config);
  request && (this.request = request);
  response && (this.response = response);
  // 因为该函数是作为构造函数使用,
  // 所以最终会默认返回this,也就是AxiosError
}

// 让AxiosError继承Error的原型
// 额外自定义了一个toJSON方法
utils.inherits(AxiosError, Error, {
  toJSON: function toJSON() {
    return {
      // Standard
      message: this.message,
      name: this.name,
      // Microsoft
      description: this.description,
      number: this.number,
      // Mozilla
      fileName: this.fileName,
      lineNumber: this.lineNumber,
      columnNumber: this.columnNumber,
      stack: this.stack,
      // Axios
      config: utils.toJSONObject(this.config),
      code: this.code,
      status:
        this.response && this.response.status ? this.response.status : null,
    };
  },
});

const prototype = AxiosError.prototype;
const descriptors = {};

// 自定义了一些异常类型
[
  'ERR_BAD_OPTION_VALUE',
  'ERR_BAD_OPTION',
  'ECONNABORTED',
  'ETIMEDOUT',
  'ERR_NETWORK',
  'ERR_FR_TOO_MANY_REDIRECTS',
  'ERR_DEPRECATED',
  'ERR_BAD_RESPONSE',
  'ERR_BAD_REQUEST',
  'ERR_CANCELED',
  'ERR_NOT_SUPPORT',
  'ERR_INVALID_URL',
  // eslint-disable-next-line func-names
].forEach((code) => {
  descriptors[code] = { value: code };
});

// 给AxiosError类增加属性
// 比如AxiosError['ERR_BAD_OPTION_VALUE']='ERR_BAD_OPTION_VALUE'
Object.defineProperties(AxiosError, descriptors);

// 给AxiosError原型增加属性
Object.defineProperty(prototype, 'isAxiosError', { value: true });

// 根据error和code创建一个AxiosError异常
// 等于是将一个普通异常转为AxiosError异常
AxiosError.from = (error, code, config, request, response, customProps) => {
  // 根据AxiosError原型创建一个异常对象
  const axiosError = Object.create(prototype);

  // 将error原型上的方法赋给axiosError
  utils.toFlatObject(
    error,
    axiosError,
    // 当取到Error时,停止
    function filter(obj) {
      return obj !== Error.prototype;
    },
    // 不复制等于isAxiosError的属性
    (prop) => {
      return prop !== 'isAxiosError';
    }
  );

  // 调用AxiosError的构造函数
  // this指向axiosError
  AxiosError.call(axiosError, error.message, code, config, request, response);

  // 设置原因为原异常
  axiosError.cause = error;

  // 使用原异常的名称
  axiosError.name = error.name;

  // 给异常传递一些自定义的属性
  customProps && Object.assign(axiosError, customProps);

  return axiosError;
};

export default AxiosError;

下面看看inherits函数是如何实现的:

// 作用:让A继承B
const inherits = (constructor, superConstructor, props, descriptors) => {
  // A的原型等于B原型
  // AxiosError.prototype=Object.create(Error.prototype)
  constructor.prototype = Object.create(
    superConstructor.prototype,
    descriptors
  );
  //A的构造函数等于它本身
  constructor.prototype.constructor = constructor;
  // 设置A的super为B的原型
  Object.defineProperty(constructor, 'super', {
    value: superConstructor.prototype,
  });
  // 给A增加自定义属性
  props && Object.assign(constructor.prototype, props);
};

上述代码写完,就可以通过new AxiosError('自定义错误')来创建一个AxiosError的异常了。

7.2 CanceledError.js

下面来看看CanceledError是怎么写的:

// CanceledError的构造函数
// 内部其实调用的是AxiosError的构造函数
// 只不过指定了code为AxiosError.ERR_CANCELED
// 并且重新指定了名称为CanceledError
function CanceledError(message, config, request) {
  AxiosError.call(
    this,
    message == null ? 'canceled' : message,
    AxiosError.ERR_CANCELED,
    config,
    request
  );
  this.name = 'CanceledError';
}

// 让CanceledError继承AxiosError的所有原型属性
// CanceledError原型扩展属性{__CANCEL__: true}
utils.inherits(CanceledError, AxiosError, {
  __CANCEL__: true,
});

export default CanceledError;

8 结束

今天关于axios如何实现请求取消的内容就到这里了,如果有疑问,欢迎大家评论区沟通交流!