axios 源码解析

796 阅读6分钟

版本说明

此博客主要是针对 axios@0.23.0做一次源码解析,主要研究以下几点:

  1. axios的初始化过程
  2. 核心请求方法Axios.prototype.request的实现
  3. 取消相关的 API 的实现。

思维导图

axios

一、初始化过程

gitdown下源码并install后,打开package.json文件,main字段声明了主入口文件的位置:

// package.json
{
  "name": "axios",
  "version": "0.23.0",
  "description": "Promise based HTTP client for the browser and node.js",
  "main": "index.js",
  "types": "index.d.ts",
  ...
}

从中可以看到主入口文件是index.js,打开index.js文件,里面只有一段代码,加载了lib文件夹下axios.js文件:

module.exports = require('./lib/axios');

所以真正的源码文件是axios.js,可以看到该文件通过调用createInstance函数并传入defaults变量,初始化了 axios 变量:

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

我们平时导入的axios的构造函数其实就是createInstance函数,我们先看这一构造函数:

function createInstance(defaultConfig) {
  // new 一个 Axios 生成实例对象,私有属性包含 defaults(参数)和 interceptors(拦截器)
  var context = new Axios(defaultConfig);

  // bind 返回一个新的 wrap 函数
  // 调用 axios 的本质其实是调用 Axios.prototype.request
  var instance = bind(Axios.prototype.request, context);

  // 复制 axios.prototype 到 instace 函数(wrap)上
  // 也就是为什么会有 axios.get 等别名方法
  // 且调用的是 Axios.prototype.get 方法
  utils.extend(instance, Axios.prototype, context);

  // 复制 axios.prototype 到 instace 函数(wrap)上
  // 也就是为什么默认配置 axios.defaults 和拦截器 axios.interceptors 可以使用的原因
  // 其实就是 new Axios().defaults 和 new Axios().interceptors
  utils.extend(instance, context);

  // 工厂模式 创建新的实例 用户可以自定义一些参数
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

1. Axios 构造函数

可以看到上面构造函数第一句就是实例化Axios实例,这一构造函数分三部分进行叙述。Axios构造函数源码位于./core/Axios.js中:

  1. 第一部分:初始化两个属性,一个默认参数defaults,另一个则是interceptors拦截器。
  2. 第二部分: Axios.prototype.request方法,该方法是所有请求真正调用的方法。
  3. 第三部分:其他请求方法,比如:deletegetpost等。

1.1 初始化属性

function Axios(instanceConfig) {
  // 默认参数
  this.defaults = instanceConfig;
  // 拦截器 请求和响应的拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

1.1.1 defaults默认参数

默认参数其实是在初始化axios时传入的,其引用的是./lib/defaults导出的defaults变量:

var defaults = {

  transitional: {
    silentJSONParsing: true,
    forcedJSONParsing: true,
    clarifyTimeoutError: false
  },

  // 默认适配器方法
  adapter: getDefaultAdapter(),


  // 请求数据转换默认方法
  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');

    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
      setContentTypeIfUnset(headers, 'application/json');
      return stringifySafely(data);
    }
    return data;
  }],

  // 响应数据转换默认方法
  transformResponse: [function transformResponse(data) {
    var transitional = this.transitional || defaults.transitional;
    var silentJSONParsing = transitional && transitional.silentJSONParsing;
    var forcedJSONParsing = transitional && transitional.forcedJSONParsing;
    var strictJSONParsing = !silentJSONParsing && this.responseType === 'json';

    if (strictJSONParsing || (forcedJSONParsing && utils.isString(data) && data.length)) {
      try {
        return JSON.parse(data);
      } catch (e) {
        if (strictJSONParsing) {
          if (e.name === 'SyntaxError') {
            throw enhanceError(e, this, 'E_JSON_PARSE');
          }
          throw e;
        }
      }
    }

    return data;
  }],

  /**
   * A timeout in milliseconds to abort a request. If set to 0 (default) a
   * timeout is not created.
   */
  // 默认延时器
  timeout: 0,

  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',

  maxContentLength: -1,
  maxBodyLength: -1,

  // 校验状态码
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  },

  // 请求头
  headers: {
    common: {
      'Accept': 'application/json, text/plain, */*'
    }
  }
};

module.exports = defaults;

可以看到里面是常用的默认配置,比如:请求头headers,请求数据转换默认方法transformRequest,适配器方法adapter等。这些默认配置可以通过axios.defaults.xx来改变(初始化过程会将实例化的Axios挂载到axios上,这些下面会讲)。

1.1.2 interceptors拦截器

可以看到interceptors是一个对象,里面有两个属性requestresponse,这两个属性的值都是实例化了一个InterceptorManager变量。该构造函数在core/InterceptorManager

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

// 初始化
function InterceptorManager() {
  this.handlers = [];
}

// use 方法:添加一个拦截器放入栈handlers中
InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null
  });
  return this.handlers.length - 1;
}

// eject方法:从栈中移除拦截器
InterceptorManager.prototype.eject = function eject(id) {
 if (this.handlers[id]) {
    this.handlers[id] = null;
  }
}

// forEach: 迭代所有拦截器,并执行
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
}

上面的代码很简单,就是定义了一个构造函数InterceptorManager,里面有一个私有属性handlers用于存放用户通过use方法创建的拦截器。eject方法用于删除handlers存在的拦截器,forEach方法遍历所有拦截器,并执行。

1.2 Axios.prototype.request方法

该方法是整个库中的核心方法,想单独拿出来讲,这里先不做阐述。

1.3 其他请求方法

// 提供一些请求方法的别名
// 遍历执行
// 这也就是我们可以用 axios.get 等别名方式的调用,实际调用的是 Axios.prototype.request 方法
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: (config || {}).data
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

想看懂上面的代码,首先得了解两个工具函数utils.forEachmergeConfig,从命名上就可以看出,一个是遍历,一个是合并配置。具体是不是呢?可以看源码:

function forEach(obj, fn) {
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // Force an array if not already something iterable
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  if (isArray(obj)) {
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // Iterate over object keys
    // 用 for in 遍历对象,但 for in 会遍历原型链上可遍历的属性
    // 所以用 hasOwnProperty 来过滤自身属性
    // 其实也可以用 Object.keys 来遍历,它不遍历原型链上可遍历的属性
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

module.exports = {
  forEach: forEach  
}

可以清除的看到utils.forEach就是遍历传入的obj来执行fn方法,类似数组中的forEach方法。

module.exports = function mergeConfig(config1, config2) {
  // eslint-disable-next-line no-param-reassign
  config2 = config2 || {};
  var config = {};
	
  ...

  // 根据两个配置项的键 key 来遍历,取到 mergeMap[key] 的值
  utils.forEach(Object.keys(config1).concat(Object.keys(config2)), function computeConfigValue(prop) {
    var merge = mergeMap[prop] || mergeDeepProperties;
    var configValue = merge(prop);
    (utils.isUndefined(configValue) && merge !== mergeDirectKeys) || (config[prop] = configValue);
  });

  return config;
};

mergeConfig方法很清晰:合并传入的两个配置,如果存在相同的配置,第二个参数中的配置覆盖第一个参数的。

看完这两个工具函数,上面Axios原型上定义的方法的逻辑也就清楚了,遍历post,get等请求字段名,并在Axios原型上创建与之对应的方法,这也就是为什么axios.get等别名方法真正调用的方法,其函数内部实际是调用request方法。

2. 生成instance实例

// bind 返回一个新的 wrap 函数
// 调用 axios 的本质其实是调用 Axios.prototype.request
var instance = bind(Axios.prototype.request, context);

// 返回一个新的函数 wrap
module.exports = function bind(fn, thisArg) {
  return function wrap() {
    // 把 arguments 对象里的数据放到数组 args 里
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }

    // 调用 fn 函数,上下文环境在 thisArg 里,参数是 args
    return fn.apply(thisArg, args);
  };
};

通过bind函数返回一个wrap函数,wrap函数内部其实主要就是通过apply方法改变this指向,将request方法的作用域挂载到Axios实例上,就可以访问它的属性和方法。

3. 别名方法的实现

// 复制 Axios.prototype 到 instace 函数(wrap)上
// 也就是为什么会有 axios.get 等别名方法
// 且调用的是 Axios.prototype.get 方法
utils.extend(instance, Axios.prototype, context);

// 遍历 b 对象上的属性,将其放到 a 对象上,如果是函数则用 bind 调用
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

forEach方法在上面已经讲过了,该步骤是通过遍历Axios.prototype对象上的属性和方法,并复制到instance(wrap)函数上,这也就是为什么能有axios.get等别名方法的原因。extend工具函数会判断传入的值是不是函数,如果是函数则会用bind绑定该函数的上下文环境,别名方法的上下文环境在Axios实例上,也就可以访问Axios的属性和方法。

4. 扩展axios属性

// 复制 axios.prototype 到 instace 函数(wrap)上
// 也就是为什么默认配置 axios.defaults 和拦截器 axios.interceptors 可以使用的原因
// 其实就是 new Axios().defaults 和 new Axios().interceptors
utils.extend(instance, context);

extend工具方法上面刚讲过,这句代码就是将context(new Axios)的属性和方法复制到instance方法上,也就是说instance方法上有Axios的属性和方法。

5. axios.create方法

// 工厂模式 创建新的实例 用户可以自定义一些参数
instance.create = function create(instanceConfig) {
  return createInstance(mergeConfig(defaultConfig, instanceConfig));
};

工厂模式,在instance函数上,创建一个create方法,该方法其实是调用createInstance方法,用户可以自定义一些参数。

6. 返回 intance 实例

return instance;

二、Axios.prototype.request实现

在初始化过程中遗留了一点没有讲到,就是所有请求真正调用的方法Axios.prototype.request,这一部分我们逐层分析该方法的实现,该源码位于core/Axios.js

1. 解析config

 // Allow for axios('example/url'[, config]) a la fetch API
  // 这一段代码 其实就是使用 axios('example/url') config 可以省略
  // 最终都是放到 config 里面
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // 合并默认参数和用户传递的参数
  config = mergeConfig(this.defaults, config);

  // Set config.method
  // 设置请求的方法,默认是 get 
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

// 异常检测 transitional,防止用户传入的格式不正确
var transitional = config.transitional;
 if (transitional !== undefined) {
  validator.assertOptions(transitional, {
      silentJSONParsing: validators.transitional(validators.boolean),
      forcedJSONParsing: validators.transitional(validators.boolean),
      clarifyTimeoutError: validators.transitional(validators.boolean)
    }, false);
 }

该段代码做了如下几件事:

  1. 判断config是否是字符串,如果是字符串则看有没有传入第二个参数,没传的话默认是空对象,并将第一个参数赋值给url属性,确保config是对象。(这一段代码 其实就是可以使用 axios('example/url')的原因);
  2. 通过mergeConfig方法合并传入的配置与默认配置,如果存在相同的配置项,传入的配置会覆盖;
  3. 定义config.method,确保是小写字母,并且默认是get请求;
  4. 异常检测 config.transitional,防止用户传入的格式不正确。

2. 拦截器的处理

回顾一下,拦截器是通过use方法以对象的形式存入handlers中,具体的参数包含:

InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null
  });
  return this.handlers.length - 1;
}

然后再看这段代码:

var requestInterceptorChain = [];
var synchronousRequestInterceptors = true;
// 遍历用户设置的请求拦截器,放到数组 requestInterceptorChain 前面
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 正在运行直接返回
    if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
      return;
    }

	// 同步请求放到 synchronousRequestInterceptors
	synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

 	// 将用户请求的函数,放到 requestInterceptorChain 数组中
	requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});

var responseInterceptorChain = [];
// 遍历用户设置的响应拦截器,放到数组 responseInterceptorChain 后面
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});

上述代码主要做了两件事,一是,遍历用户设置的请求拦截器,通过unshift方法插入数组 requestInterceptorChain 前面;二是,遍历用户设置的响应拦截器,通过push方法放到数组 responseInterceptorChain 后面。

3. 处理异步请求

 var promise;

 // 不是同步请求
 if (!synchronousRequestInterceptors) {
    
    // 组成 `promise`链
    // 将 xhr 请求的 dispatchRequest 和 undefined 放到同一数组中
    var chain = [dispatchRequest, undefined];

    // 将用户设置的请求拦截器放到 chain 最前面
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    // 将用户设置的响应拦截器放到 chain 后面
    chain = chain.concat(responseInterceptorChain);

    // 创建 promise 实例
    promise = Promise.resolve(config);

    // 遍历 chain 数组
    while (chain.length) {
      // 两两对应移出来,放到 then 的两个参数里
      promise = promise.then(chain.shift(), chain.shift());
    }

    return promise;
 }

如果用户传入的拦截器配置synchronous属性为true则说明是同步,如果没有传入或者传入的是false,则走这段异步请求,默认的是异步请求。这段代码做了如下四件事:

  1. xhr 请求的 dispatchRequestundefined 放到同一数组中,为什么要放个undefined进去?请看第三步。
  2. 将用户设置的请求拦截器放到 chain 最前面,将用户设置的响应拦截器放到 chain 后面;
  3. 创建 promise 实例,遍历chain,两两对应移出来调用,放到微任务队列里;
  4. 返回promise,做链式调用。

总体来说就是,先去处理拦截器的请求的函数,然后处理dispatchRequest函数,最后处理拦截器响应的函数。因为拦截器的请求和响应函数是用户自定义的,所以得自己研究,dispatchRequest函数是源码自带的,我们可以看看该函数具体做了什么。

4. dispatchRequest函数

因为同步请求和异步请求都有该函数所以放到一层来讲,不单独在异步请求下讲,这一函数将分为三部分阐述,该源码位于core/dispatchRequest.js

第一部分:处理请求的配置,例如:处理config.headers,转换config.data里的数据,删除请求头里的请求方法;

第二部分:适配器adapter函数;

第三部分:服务器交互成功与失败的处理。

4.1 处理请求的配置

 // 如果取消被请求,则抛出一个取消
  throwIfCancellationRequested(config);

  // 确保 headers 存在
  config.headers = config.headers || {};

  // 转换请求的数据
  config.data = transformData.call(
    config,
    config.data,
    config.headers,
    config.transformRequest
  );

  // 拍平 headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );
 
  // 删除 headers 里面的这些方法
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

上述代码做了如下几件事:

  1. 如果取消被请求,则抛出一个取消;
  2. 转换请求的数据,拍平headers
  3. 删除headers里的请求方法。

我们主要看下请求转换数据的方法transformDataheaders拍平的方法,先来看transformData方法:

module.exports = function transformData(data, headers, fns) {
  var context = this || defaults;
  /*eslint no-param-reassign:0*/
  utils.forEach(fns, function transform(fn) {
    data = fn.call(context, data, headers);
  });

  return data;
};

可以看到该方法是遍历传入的fns参数,然后执行里面的每个方法。fns就是上面传入的config.transformRequesttransformRequest是可以自己定义的,也可以使用默认配置里面的defaults用来处理请求头的数据。另一是工具函数merge

/**
 * 接受对象,合并属性
 * Example:
 *
 * ```js
 * var result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // outputs 456
 * ```
 */
function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      result[key] = merge(result[key], val);
    } else if (isPlainObject(val)) {
      result[key] = merge({}, val);
    } else if (isArray(val)) {
      result[key] = val.slice();
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

该方法通过遍历传进来的参数,执行assignValue方法,将每个对象上的属性放到result中,并返回。也就是说假如,后面对象与前面对象有相同的属性,后面对象的属性会覆盖前面对象的属性。

4.2 adapter函数

// 适配器
var adapter = config.adapter || defaults.adapter;

可以根据规定自定义适配器方法,也可以使用默认配置中的适配器方法,在default.js中,分别处理了浏览器环境和node环境的适配器方法,我们主要看浏览器环境的适配器方法:

/**
 * 判断是浏览器环境还是 node 环境
 *  1. 浏览器环境会用 xhr 生成适配器
 *  2. node 环境生成适配器的方法,暂时还未看 
 */
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;
}

可以看到浏览器适配器方法是在./adapters/xhr.js中,具体可以看看做了什么:

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // 获取基础配置
    var requestData = config.data;
    var requestHeaders = config.headers;
    var responseType = config.responseType;
    var onCanceled;
    
    function done() {
      if (config.cancelToken) {
        config.cancelToken.unsubscribe(onCanceled);
      }

      if (config.signal) {
        config.signal.removeEventListener('abort', onCanceled);
      }
    }

    // 如果传递进来的 data 是表单数据,则删除请求头 Content-Type 属性
    if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }

    // XHR 对象用于与服务器交互
    var request = new XMLHttpRequest();

    // HTTP basic authentication
    // HTTP 基本认证
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }


    var fullPath = buildFullPath(config.baseURL, config.url);
    // 初始化一个请求
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // Set the request timeout in MS
    request.timeout = config.timeout;

    function onloadend() {
      if (!request) {
        return;
      }
      // Prepare the response
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      var responseData = !responseType || responseType === 'text' ||  responseType === 'json' ?
        request.responseText : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };

      settle(function _resolve(value) {
        resolve(value);
        done();
      }, function _reject(err) {
        reject(err);
        done();
      }, response);

      // Clean up request
      request = null;
    }

    if ('onloadend' in request) {
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
      // Listen for ready state to emulate onloadend
      // 监听 state 改变
      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;
        }
        // readystate handler is calling before onerror or ontimeout handlers,
        // so we should call onloadend on the next 'tick'
        setTimeout(onloadend);
      };
    }

    // Handle browser request cancellation (as opposed to a manual cancellation)
    // 取消
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }

      reject(createError('Request aborted', config, 'ECONNABORTED', request));

      // Clean up request
      request = null;
    };

    // Handle low level network errors
    // 错误
    request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(createError('Network Error', config, null, request));

      // Clean up request
      request = null;
    };

    // Handle timeout
    // 超时
    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
      var transitional = config.transitional || defaults.transitional;
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(createError(
        timeoutErrorMessage,
        config,
        transitional.clarifyTimeoutError ? 'ETIMEDOUT' : 'ECONNABORTED',
        request));

      // Clean up request
      request = null;
    };

    // Add xsrf header
    // This is only done if running in a standard browser environment.
    // Specifically not if we're in a web worker, or react-native.
    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;
      }
    }

    // Add headers to the request
    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);
        }
      });
    }

    // Add withCredentials to request if needed
    // 跨域时携带 cookies
    // 一个 布尔值,用来指定跨域 Access-Control 请求是否应当带有授权信息,如 cookie 或授权 header 头
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }

    // Add responseType to request if needed
    if (responseType && responseType !== 'json') {
      request.responseType = config.responseType;
    }

    // Handle progress if needed
    // 上传下载进度相关
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }

    // Not all browsers support upload events
    // 区别不支持上传事件的浏览器
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }

    // 取消的条件
    if (config.cancelToken || config.signal) {
      // Handle cancellation
      // eslint-disable-next-line func-names
      onCanceled = function(cancel) {
        if (!request) {
          return;
        }
        reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
        request.abort();
        request = null;
      };

      config.cancelToken && config.cancelToken.subscribe(onCanceled);
      if (config.signal) {
        config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
      }
    }

    if (!requestData) {
      requestData = null;
    }

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

可以看到该方法还是挺长的,但是骚年别放弃啊,我们可以大体看看做了什么,总结如下:

  1. 返回了一个promise实例,可以链式调用;
  2. 处理config
  3. 实例化了一个XHR对象,用于和服务器交互,后面就是XHR对象的基本操作,不了解的可以看看MDN的介绍;
  4. 取消的处理(放到取消部分再讲)。

4.3 服务器交互成功与失败的处理

 return adapter(config).then(function onAdapterResolution(response) {
    // 有可能是执行到这里被取消,取消的情况执行这句
    throwIfCancellationRequested(config);

    // Transform response data
    // 转换响应的数据
    response.data = transformData.call(
      config,
      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.call(
          config,
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
 });

成功和失败都会执行config.transformResponse的函数,成功会将处理后的数据放在response.data中,失败则会走Promise.reject(reason),会走链式里的reject方法。这个中间处理方法也大体讲完了,里面细节需要自己去调试去看。

5. 同步请求处理

// 从 requestInterceptorChain 数组中,取出每个请求,并依次执行
var newConfig = config;
while (requestInterceptorChain.length) {
  var onFulfilled = requestInterceptorChain.shift();
  var onRejected = requestInterceptorChain.shift();
  try {
    newConfig = onFulfilled(newConfig);
  } catch (error) {
    onRejected(error);
    break;
  }
}

// 调用 dispatchRequest 方法
try {
  promise = dispatchRequest(newConfig);
} catch (error) {
  return Promise.reject(error);
}

// 走响应成功的方法
while (responseInterceptorChain.length) {
   promise = promise.then(responseInterceptorChain.shift(),       responseInterceptorChain.shift());
}

// 返回 promise 实例用于链式调用
return promise;

很简单,流程就是:处理所有拦截器request方法 -> dispatchRequest与服务器交互 -> 拦截器response方法。

三、取消相关API的实现

./lib/axios.js中初始化axios默认实例后,又给axios函数挂载了一些属性和方法:

xios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.VERSION = require('./env/data').version;

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');

// Expose isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');

module.exports = axios;

我们主要看这个axios.CancelToken取消令牌函数,这个函数可以取消请求,具体用法如下:

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
  }
});

source.cancel('取消操作~');

可以看到使用了构造函数的静态方法source,该方法在./cancel/CancelToken.js

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

token就是CancelToken的实例,可以看看实例化时都做了什么:

function CancelToken(executor) {
  // 类型判断,传入的执行器必须是函数
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;

  // 定义了 this.promise,并将 resolve 赋给全局变量 resolvePromise
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
 
  // 存入微任务队列
  this.promise.then(function(cancel) {
    if (!token._listeners) return;

    var i;
    var l = token._listeners.length;

    for (i = 0; i < l; i++) {
      token._listeners[i](cancel);
    }
    token._listeners = null;
  });

  // then 重新赋值
  this.promise.then = function(onfulfilled) {
    var _resolve;
    // eslint-disable-next-line func-names
    var promise = new Promise(function(resolve) {
      token.subscribe(resolve);
      _resolve = resolve;
    }).then(onfulfilled);

    promise.cancel = function reject() {
      token.unsubscribe(_resolve);
    };

    return promise;
  };
 
  // 执行执行器方法
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

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

主要做了如下几件事:

  1. 类型判断,传入的执行器必须是函数;
  2. 定义了 this.promise,并将 resolve 赋给全局变量 resolvePromise
  3. then里面执行token._listeners里面的所有函数。
  4. then 重新赋值;
  5. 执行执行器方法,将cancel方法赋值给source里面的cancel属性。

可以看到then里面有个token._listeners属性,这个属性在哪生成呢?是在适配器那部分代码中有一段:

// 取消的条件
if (config.cancelToken || config.signal) {
 // Handle cancellation
 // eslint-disable-next-line func-names
 onCanceled = function(cancel) {
   if (!request) {
      return;
   }
   reject(!cancel || (cancel && cancel.type) ? new Cancel('canceled') : cancel);
   request.abort();
   request = null;
 };

 config.cancelToken && config.cancelToken.subscribe(onCanceled);
 if (config.signal) {
   config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
   }
 }

根据传入的配置是否存在cancelToken属性,来调用subscribe方法,而subscribe方法就是生成_listeners

属性的。

CancelToken.prototype.subscribe = function subscribe(listener) {
  if (this.reason) {
    listener(this.reason);
    return;
  }

  if (this._listeners) {
    this._listeners.push(listener);
  } else {
    this._listeners = [listener];
  }
};

也就是说,当执行then里面微任务时,会执行onCanceled方法,该方法中又有一句request.abort(),则会触发requestonabort事件,取消请求。

request.onabort = function handleAbort() {
  if (!request) {
    return;
  }

  reject(createError('Request aborted', config, 'ECONNABORTED', request));

  // Clean up request
  request = null;
};

到这,整个取消请求的流程已经讲解完了,细节还是需要自己去看~

欢迎提错,共同学习~~

参考链接

  1. axios官方文档
  2. 若川axios源码解析