Axios 源码浅析

Posted by lainlee on 09-06,2021

引言

熟悉 Vue 的同学们,肯定都用过 Axios , 简洁的 API 语法和方便的拦截器都受到了开发者的热烈欢迎,今天我们就来探索一下他背后的实现原理

本文分析的 Axios 版本为 0.21.0

回顾

首先我们回顾一下常用的 Axios 功能都有哪些

  • axios(config) / axios.get / axios.post / axios.delete ....

    调用 axios ,传参发起一个请求,或者直接发起 get、post、delete 等请求

  • axios.create

    创建一个新的 axios 实例

  • axios.defaults.xxxx

    配置 axios 的默认配置

  • axios.interceptors.request / axios.interceptors.response

    配置 axios 的全局拦截器,也可以只配置某个通过 create 创建的实例的拦截器

  • cancel

    取消某个请求

接下来我们根据源码一一解析,上述的功能是如何实现的

代码结构

源代码里的主要目录是 lib ,其余的是一些单元测试、文档之类的,这里不再赘述

lib
│  axios.js
│  defaults.js
│  utils.js
│
├─adapters
│      http.js
│      README.md
│      xhr.js
│
├─cancel
│      Cancel.js
│      CancelToken.js
│      isCancel.js
│
├─core
│      Axios.js
│      buildFullPath.js
│      createError.js
│      dispatchRequest.js
│      enhanceError.js
│      InterceptorManager.js
│      mergeConfig.js
│      README.md
│      settle.js
│      transformData.js
│
└─helpers
        bind.js
        buildURL.js
        combineURLs.js
        cookies.js
        deprecatedMethod.js
        isAbsoluteURL.js
        isAxiosError.js
        isURLSameOrigin.js
        normalizeHeaderName.js
        parseHeaders.js
        README.md
        spread.js

这里有几个重要文件(axios.js、defaults.js、utils.js)以及几类文件分别放在四个文件夹里

  • axios.js

    该文件作为入口文件,主要作为生成并导出 axios 对象、以及扩展一系列例如 axios.create 的功能方法

  • defaults.js

    该文件是默认配置

  • utils.js

    该文件是一系列辅助方法,例如:isStirng、extend 等等

  • adapter

    该目录里主要存放一些与 ajax 的适配器,也就是封装原生的 xmlHttpRequest 或者 node 的 http 库等,然后处理成方便使用的自己的方法,如果以后有更新的 ajax 方法,例如 fetch,那么 Axios 只需要修改这部分去适配新的接口即可

  • cancel

    这里实现取消请求的功能

  • core

    这是 Axios 的核心代码,包括 Axios 的基类、一些错误的封装等等,其中比较核心的又有 Axios.js、dispatchRequest.js、InterceptorManager.js

  • helpers

    这里另一部分辅助的功能性模块,和 utils.js 的区别可能是因为 utils.js 是更加通用的方法,而这个目录里的则是定制的一些辅助方法

adapter 目录的作用,是因为使用了设计模式里的适配器模式,有兴趣的童鞋可以延伸阅读一下

axios(config) / axios.get / axios.create 等方法实现

我们暂时先不关心具体的实现,首先来看如何将抽象方法暴露出来提供开发者使用,这里主要关注 lib/axios.js 这个文件

// path: lib/axios.js

var utils = require('./utils');
var bind = require('./helpers/bind');
var Axios = require('./core/Axios');
var mergeConfig = require('./core/mergeConfig');
var defaults = require('./defaults');

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

axios.Cancel = ...
axios.CancelToken = ...
axios.isCancel = ...
axios.all = ...
axios.spread = ...
axios.isAxiosError = ...

module.exports = axios;

可以看出,我们开发者使用的 axios,也是通过 Axios 基类生成的一个实例(严格意义上说并不是一个单纯的实例)

可能有的同学要问了,为何不直接 export 一个实例呢,还封装一个函数,有什么用意呢,其实这里的目的是在导出实例的同时,将我们标题上说的 axios(config) / axios.get / axios.create 等方法也绑定上去,扩展这个实例,使它的功能更强大,我们接下来一步步看是怎么实现的:

  • var context = new Axios(defaultConfig);
    var instance = bind(Axios.prototype.request, context);
    

    这两行先是生成一个实例 context,然后通过 bind 函数将基类 Axios 的 request 方法的上下文 this 绑定为实例 context,并生成一个新的方法 instance,也就是未来我们用的 axios 对象,到这一步,axios 对象本质上还只是一个 request 方法

    bind 函数相当于 es6 的 Function.prototype.bind 方法,只不过为了兼容性,axios 在这里自己实现了一遍

  • utils.extend(instance, Axios.prototype, context);
    

    这一步,通过 extend 方法,将 Axios.prototype ,也就是基类的原型链,合并(继承)到 instance(request) 对象上,这之中当然是冗余的,也就是 instance 的原型上也包含自身 request 方法,到这里就实现了 axios(config)(相当于 axios.request(config))以及 axios.get / axios.post ... (原型链方法)这几个功能

    这里 extend 函数的第三个参数是 context,作用是将 context 对象当作 Axios.prototype 函数的上下文 this,extend 也是自己实现的方法,感兴趣的同学可以自行查看

  • utils.extend(instance, context)
    

    这一步,还是通过 extend 方法,将 context(实例)的其他属性,例如:defaults、interceptors 合并到 instance 对象上

  • axios.create = function create(instanceConfig) {
      return createInstance(mergeConfig(axios.defaults, instanceConfig));
    };
    

    这里将 createInstance 函数扩展为 axios.create ,允许开发者生成新的实例,同样的,也扩展了 Cancel,all 等方法

最后,我们得到了一个对象 axios ,它看起来像 Axios 类的实例,用起来也像实例,然而它只是一个 request 方法,但是拥有实例的属性和原型,以及一些其他的 API

年少的我曾经想当然的认为,通过 axios.create 创建的实例,还可以继续通过 create 创建子类,现在看了源码之后终于悟了,原来根本就没有这层设计

axios 的默认配置

接下来我们在深入具体逻辑之前,先看一下,Axios 都有什么默认配置,如何实现修改全局配置,以及区分实例的配置

再回看一下上面的代码以及 Axios 基类

// lib/core/Axios.js
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
// lib/axios.js
var defaults = require('./defaults');
// Create the default instance to be exported
var axios = createInstance(defaults);

// Factory for creating new instances
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

全局配置是一个文件,通过 new 一个实例,绑定到 axios 对象上的 defaults 属性上,而之后通过 axios.create 创建的实例,都要和 axios.defaults 合并之后再实例化,因此,axios.defaluts 就变成了一个全局配置,修改该属性会影响 axios 对象以及通过它派生出来的所有实例,而实例上有自己的 defaults 对象,修改它只会影响自己

接下来我们看一下具体的默认配置都有哪些

var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

var defaults = {
  adapter: getDefaultAdapter(),
  transformRequest: [...],
  transformResponse: [...],
  timeout: 0,
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  maxContentLength: -1,
  maxBodyLength: -1,
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};
defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
module.exports = defaults;

这里省略了一些细节,具体可以去源代码里查看,但是看到这里是不是很多同学反应过来了,原来文档里说的根本不是默认配置,只是列举了一下所有可用的配置以及应该传什么值,而真正的默认配置只有上面文件里的这些

  • adapter

    这里区分了浏览器环境还是 Node 环境,这里分别匹配了 lib/adapters/xhr.js 和 lib/adapters/http.js 文件,也允许开发者自己做适配,反正我还没有适配过。。

  • transformRequest 和 transformResponse

    这里都有默认配置,判断分支还挺多,如果修改配置的话,直接赋值,这些逻辑就都没了,个人感觉这里设计的不是很好,所以如果只是想增加新的 transform 规则的话,我建议在默认的基础上新增:axios.defaults.transformRequest = [ ...axios.defaults.transformRequest, ...customerTransform ]

  • timeout、xsrfCookieName、xsrfHeaderName、maxContentLength、maxBodyLength、validateStatus

    这些配置都很容易理解,这里不赘述

  • headers

    默认的 header 头,有一个通用 Accept 头,然后具体区分了一下请求类型,只有 'post', 'put', 'patch' 这三种请求会带上默认的 'Content-Type': 'application/x-www-form-urlencoded',看清楚了啊,可不是 'application/json' (狗头)

axios.request

接下来我们看看发请求的核心逻辑,这部分逻辑由 3 个部分组成,我们一层层看下去

request

// lib/core/Axios.js
var dispatchRequest = require('./dispatchRequest');

Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

这段代码其实不难理解,我们分解一下

  • if (typeof config === 'string') {
      config = arguments[1] || {};
      config.url = arguments[0];
    } else {
      config = config || {};
    }
    
    config = mergeConfig(this.defaults, config);
    
    if (config.method) {
      config.method = config.method.toLowerCase();
    } else if (this.defaults.method) {
      config.method = this.defaults.method.toLowerCase();
    } else {
      config.method = 'get';
    }
    

    这里先做了一下兼容,如果 config 是字符串,那么就认为它是 config 对象的 url 属性,最终使用的还是 config 对象,保证数据结构的一致性,然后将传参与本实例的默认配置合并,并保证 method 属性一定有,至少是 'get'

    这里特意判断 method,一是为了格式化为小写,再一个默认配置里没有 method 的配置,所以为了不能为空,必须赋一个值,不过这里我不是很理解为何不给 method 一个默认值。。

  • var chain = [dispatchRequest, undefined];
    var promise = Promise.resolve(config);
    
    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
      chain.unshift(interceptor.fulfilled, interceptor.rejected);
    });
    
    this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
      chain.push(interceptor.fulfilled, interceptor.rejected);
    });
    
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }
    
    return promise;
    

    dispatchRequest 是真正发请求的抽象方法,我们会在下面详细说明,而 interceptors.request 和 interceptors.response 则是请求和响应的拦截器,连续两个方法为一组,分别对应 then 和 catch 中,每个方法都返回一个 Promise,放到 chain 数组中,然后通过循环这个数组,生成一个 Promise 调用链,中间只要有一步抛异常,就会走到最近的 catch 中,如果 catch 返回了一个 resolve 状态的 Promise,那么调用链还可以继续往下走,有点迷惑的童鞋可以看下图帮助理解

拦截器调用链

这里虽然文档没有明说,但是分析代码可以看出,请求拦截器 interceptors.request 我们使用的时候是顺序加入到 InterceptorManager 类里,但是调用的时候却是顺序循环并通过 Array.prototype.unshift 到调用链数组里,那么也就是说第一个加入的拦截器会放在最后一个去调用,实际上也确实如此,这里可能有点小坑,如果加了多个请求拦截器规则又需要有顺序的话,一定要倒着写,不然无法得到想要的结果

dispatchRequest

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Ensure headers exist
  config.headers = config.headers || {};

  // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // Flatten headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );

  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};

这里还有 Cancel 的代码,我们暂且略过,着重看请求部分

  • config.data = transformData(
      config.data,
      config.headers,
      config.transformRequest
    );
    

    这里通过 transformData 这个方法,将配置中的 config.transformRequest 方法,应用到 config.data 中,关于 transformRequest,可以参见文档

  • config.headers = utils.merge(
      config.headers.common || {},
      config.headers[config.method] || {},
      config.headers
    );
    
    utils.forEach(
      ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
      function cleanHeaderConfig(method) {
        delete config.headers[method];
      }
    );
    

    这部分合并了不同来源的 headers,都有什么来源呢,代码里也能很清晰的看到,包括通用配置 config.headers.common、特定方法的配置 config.headers[config.method](也就是config.headers.get / config.headers.post 等等....),以及当下请求传来的 headers 配置,最后,因为 common,get,post 等等这些配置都写在 headers 里,发请求时是不需要的,所以通通删了,当然这里 merge 方法是个深拷贝,所以随便删,不影响原对象

  • return adapter(config).then(function onAdapterResolution(response) {
      throwIfCancellationRequested(config);
    
      // Transform response data
      response.data = transformData(
        response.data,
        response.headers,
        config.transformResponse
      );
    
      return response;
    }, function onAdapterRejection(reason) {
      if (!isCancel(reason)) {
        throwIfCancellationRequested(config);
    
        // Transform response data
        if (reason && reason.response) {
          reason.response.data = transformData(
            reason.response.data,
            reason.response.headers,
            config.transformResponse
          );
        }
      }
    
      return Promise.reject(reason);
    });
    

    最后这部分也很好理解,调用适配器,把 config 传过去,然后处理 then,或者 catch 步骤,这里对结果应用了 config.transformResponse 方法

adapter

发请求的最终过程,还是在不同的适配器里实现,因为我个人用 Node 比较少,这里就看一下浏览器的适配器,也就是 lib/adapters/xhr.js

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;

    // formData 时删除 Content-Type
    if (utils.isFormData(requestData)) {...}

    var request = new XMLHttpRequest();

    // 设置 header Authorization
    if (config.auth) {...}

    var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // 设置超时时间
    request.timeout = config.timeout;

    // 监听 xhr 各种事件
    request.onreadystatechange = function handleLoad() {...};
    request.onabort = function handleAbort() {...};
    request.onerror = function handleError() {...};
    request.ontimeout = function handleTimeout() {...};

    // 应对 xsrf/csrf 攻击,可以配置将 cookie 放在 header 中
    if (utils.isStandardBrowserEnv()) {...}

    // 将其他 config.headers 设置到 header 中
    if ('setRequestHeader' in request) {...}

    // 设置 withCredentials 属性
    if (!utils.isUndefined(config.withCredentials)) {...}

    // 设置 responseType
    if (config.responseType) {...}

    // 设置上传文件的进度
    if (typeof config.onDownloadProgress === 'function') {...}
    if (typeof config.onUploadProgress === 'function' && request.upload) {...}

    // 取消请求的操作
    if (config.cancelToken) {...}

    // 请求主体
    if (!requestData) {
      requestData = null;
    }

    // 发请求
    request.send(requestData);
  });
};

这里我将大部分代码都省略了,这样对整个适配过程更加清楚,基本的过程我都标注在上面代码里的注释了,接下来我们一步一步的细看

  • if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }
    

    如果是 FormData,就删除 Content-Type,让浏览器自己设置,这里一般都会设置成 multipart/form-data

  • if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }
    

    这里提供了一个快速设置 HTTP 认证的方法,设置方法就是通过用户名和密码,在请求头里增加 Authorization 字段,值为 Basic 加 base64 编码后的用户名密码字符串,具体可以参见 HTTP 身份认证

  • var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    

    这里初始化一个请求,包括请求方法,请求地址,是否为异步等等,请求方法全部转为小写,请求地址是参数和地址拼起来的,就如:xxx?a=b&c=d

  • request.onreadystatechange = function handleLoad() {
      if (!request || request.readyState !== 4) {
        return;
      }
    
      // The request errored out and we didn't get a response, this will be
      // handled by onerror instead
      // With one exception: request that using file: protocol, most browsers
      // will return status as 0 even though it's a successful request
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }
    
      // Prepare the response
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };
    
      settle(resolve, reject, response);
    
      // Clean up request
      request = null;
    };
    
    module.exports = function settle(resolve, reject, response) {
      var validateStatus = response.config.validateStatus;
      if (!response.status || !validateStatus || validateStatus(response.status)) {
        resolve(response);
      } else {
        reject(createError(
          'Request failed with status code ' + response.status,
          response.config,
          null,
          response.request,
          response
        ));
      }
    };
    

    这里主要看一下 onreadystatechange ,也就是请求响应之后的处理,我们注意到在判断 status 的时候有一句注释,很重要,就是说:如果请求发生错误了,是通过 onerror 这个 handler 来处理的,但是有一种例外,那就是用 file: 发起的请求,大多数浏览器会返回 status:0,即使这个请求成功了,我们知道,一般 HTTP 状态码,200 表示成功,所以,这里代码特殊判断了一下

    其余的就正常处理,将 resolve 和 reject 交给 settle 方法来处理,这里判断逻辑也可以定义自己的 validateStatus 方法

    最后还有几个额外的处理,虽然不影响主流程,但是还是可以看一下

  • if (utils.isStandardBrowserEnv()) {
      // Add xsrf header
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;
    
      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }
    

    如果你配置了 xsrfHeaderName 和 xsrfCookieName 这个属性,那么在发请求时,会自动读取 cookie 中的相应值并带到 header 中

  • var requestHeaders = config.headers;
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
          // Remove Content-Type if data is undefined
          delete requestHeaders[key];
        } else {
          // Otherwise add header to the request
          request.setRequestHeader(key, val);
        }
      });
    }
    

    如果设置了 headers 属性,则设置到请求头里,注意,这里如果请求体没有数据的话,会直接删掉 content-type 头

最后发送 ajax 请求,如果没有 requestData ,则 send(null)

取消一个请求

之前的几个代码里,我们都跳过了取消请求相关的处理逻辑,因为跟主题的逻辑关系不大,在这里,我们统一解析一下

用法

我们先回顾一下,Cancel 这个功能是怎么用的,可能大部分人都没用过

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 {
    // handle error
  }
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

首先,通过 CancelToken.source() 方法生成一个 source 对象,source 对象里有两个属性,一个是 token,一个是 cancel 方法,请求的时候将 token 传到配置里,即可在任意时刻通过调用 cancel 方法来取消请求

所以,这个 source 就是取消的核心

Cancel 核心代码

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

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

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

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

解析:

CancelToken.source 方法,通过 CancelToken 生成一个 token,并且生成的同时, CancelToken 可以传一个回调函数,将某些方法绑定到了 cancel 属性上

继续看 CancelToken 工厂,通过 new 操作符调用的时候,会返回实例本身,也就是说,token 就是 CancelToken 的一个实例对象

在初始化过程中,token 对象会有 promise 与 reason 两个属性

首先将 resolvePromise 变量绑定为一个 promise 属性的 resolve 方法,接着在回调函数中传一个 cancel 方法(也就是 CancelToken.source().cancel 方法),当 cancel 被调用的时候,会 resolve Promise,同时,token 这个对象中的 reason 属性也可能会有值,有值的话即代表已经取消过了,可以防止重复使用

token 对象还有一个 throwIfRequested 方法,即通过判断是否有 reason 值,来抛一个异常,使请求直接走到 catch 阶段

两外两个文件 lib/cancel/Cancel.js 和 lib/cancel/isCancel.js 这里就不详细说明了,代码很少,基本是用某个实例的静态属性用来判断是否已经取消的逻辑,大家可以自行看一下

请求中使用 Cancel

之前我们分析发请求的过程中,出现了 Cancel 逻辑,一起将这部分补完

// lib/axios.js
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

这里主要是导出了生产 source 的工厂 CancelToken

// lib/core/dispatchRequest.js
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  ...

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    ...
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      ...
    }
    return Promise.reject(reason);
  });
}

这里在发请求的初始化、请求成功、请求失败时都通过 throwIfRequested 判断了用户是否调用过 cancel 方法,一旦判断调用过,直接抛出异常,走到 catch 流程,不管请求状态如何都不去处理了

throwIfRequested 方法只有当 token 有 reason 属性的时候,才会抛异常,当没有调用 cancel 方法的时候,token 永远没有 reason 属性,因此 throwIfRequested 可以在任何时候调用,而不用担心请求是否被取消

// lib/adapters/xhr.js 
if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

在适配器里,初始化 ajax 对象的时候,绑定一个 promise,大家还记得 token 对象里有一个 promise 属性吧,就是用在这里的,当这个 promise 被 resolve 的时候,也就是用户调用了 cancel 方法,这里会通过闭包获取到 ajax 对象,然后调用 request.abort() ,这样在 xhr 流程中,也中断了请求的继续发送,同时将整个适配器的状态置为 reject

到这里,请求的所有阶段就都有了 cancel 的参与,在任何时候取消,都可以直接走入 reject 流程

结语

以上就是 Axios 的核心功能,其实逻辑并不难理解,主要是封装思路以及一些设计模式值得我们学习,另外,也可以了解一些请求过程中的坑(比如:file 开头的请求),以后在使用的过程中,如果遇到了文档里看不到的问题,不妨在源码里寻找答案,如果有新的想法,也可以直接去 GitHub 提需求,希望大家不要畏惧看源码,毕竟,源码写的要比我们接手的项目更加优美,不是吗?