Axios基本原理深度解析

4,671 阅读20分钟

Axios是一个很常用的http请求库,功能强大,应用也很灵活,可用于node和浏览器环境。也可以扩展自己的请求方法。这么好用强大的工具,原理是不是很复杂呢?

NO NO NO,并不复杂

其核心只有两点:

1、request方法,Axios外部方法其实都是在调用这一个方法

2、方法内部创建一个Promise链式调用,常用的功能,拦截器,数据修改器,http请求,就是在这个Promise链式调用中逐步被执行。request方法返回Promise链。我们用的就是这个返回的Promise,执行结果就在这个Promise中的状态值中。

如果您对Promise还不是很熟悉,建议先学习之后再研究axios的原理。

深入分析原理之前,先了解一下Axios库中都提供了什么功能,根据这些功能。我们在看这个功能是怎么实现的,进而就可以了解Axios实现的原理了。

一、 Axios提供的功能

  1. http请求
    用来发送http请求的方法。例如axios(config)
  2. 并发请求
    用来同时处理多个axios请求axios.all()
  3. 拦截器(interceptors)
    请求拦截器(interceptors.request)是指可以拦截住每次或指定http请求,并可修改配置项
    响应拦截器(interceptors.response)可以在每次http请求后拦截住每次或指定http请求,并可修改返回结果项。
  4. 数据修改器(transformRequest、transformResponse)
    请求转换器(transformRequest)是指在请求前对数据进行转换,
    响应转换器(transformResponse)主要对请求响应后的响应体做数据转换。
  5. 取消请求
    通过config中的CancelToken属性,控制取消axios

二、Axios项目结构


上面的目录结构中的文件没有完全列出来。这些是整个流程中的核心,其他的功能性的代码。在理解了基本原理之后,大家去看其他代码就非常容易了。

三、基本内部工具方法介绍

在源码中会用到一些工具方法,简单介绍一下,有助于顺利理解整个流程
一、bind:给函数指定this
二、forEach:遍历数组或对象,遍历对象属于自己的属性hasOwnProperty

三、merge:深度合并多个对象

四、extend:将一个对象的方法和属性扩展到另外一个对象上,并指定this

五、mergeConfig:深度合并config的方法


接下来开始分析Axios原理:

四、多种形式调用Axios的原理

首先,我们在使用Axios的时候,会有很多种用法。是怎么实现的呢?我们先看看axios都能怎么调用

// 第一种方法
axios(config)
// 第二种
axios('example/url'[, config])
// 第三种
axios.request(config)
// 第四种
axios.get(url[, config])//'delete', 'get', 'head', 'options'请求方法一样的调用方式
// 第五种
axios.post(url[, data[, config]])// 'post', 'put', 'patch'请求方法永阳的调用方式
// 第六种
axios.all([axios1, axios2, axios3]).then(axios.spread(function (axios1response, axios2response, axios3response) {
    // 三个请求现在都执行完成
  }));
// 还可以通过axios.create方法建立自定义全局默认配置的Axios实例
axios.create(config)

总结有六种调用方式

如何实现的呢?来看看axios设置对外接口的源码lib / axios.js,把主要的代码复制出来

function createInstance(defaultConfig) {
  // 建立Axios对象
  var context = new Axios(defaultConfig);

  // Axios作者的目的是提供一个对外可用的方法。
  // 并且方法中需要用到Axios对象中的config属性和拦截器。
  // 所以要把axios原型上的方法单独拿出来,绑定context这个axios实例。
  // instance方法就是后面导出的axios实例,
  // 所以到这里位置 第一种调用方法 axios(config) 就实现了
  // 在request方法的内部,有对传入参数类型的判断,如果传入第一参数为字符串,则认为是url字符串,并且把url参数添加到第二个参数config中
  // 所以就实现了第二种调用方法axios('example/url'[, config])
  var instance = bind(Axios.prototype.request, context);

  // 这里把Axios原型上的方法和属性,扩展到instance方法上,
  // 并制定了原型上方法的this为context(上面定义axios对象)
  // Axios上有request方法,这里绑定了this为context
  // 所有第三种调用方法 axios.request(config) 就实现了
  // Axios原型中其实定义了get,post,option等等方法,
  // 所以第四种axios.get(url[, config])和第五种axios.post(url[, data[, config]])方法就实现了
  utils.extend(instance, Axios.prototype, context);

  // 这里把上面建立axios对象(context)中自有的属性方法,扩展到了instance中
  // 这样instance就有了defaults、interceptors 属性,就可以添加拦截器,操作defaultConfig了
  utils.extend(instance, context);

  return instance;
}

// 调用createInstance方法,建立了Axios实例
var axios = createInstance(defaults);

// 这里也调用上面的createInstance方法,同样建立了Axios实例,
// 只不过,这里配置了自己的config作为全局默认的config
// 所以这里实现了,通过axios.create方法建立自定义默认配置的Axios实例
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// 这里添加了all方法,其实就是promise的all方法,
// 这就是第六种调用方法,并发请求的实现原理
axios.all = function all(promises) {
  return Promise.all(promises);
};
// spread方法就是把用数组作为一个参数,变成数组的每一项为一个参数。就是为了用着方便。
axios.spread = require('./helpers/spread');

module.exports = axios;// 对外导出实例

上面注释中提到的第二种调用方法axios('example/url'[, config])的实现是request内部做了判断所以才能那么用。第四种axios.get(url[, config])和第五种axios.post(url[, data[, config]])方法的实现是因为Axios原型中定义了相应的方法,所以才得以实现。那么我们就看看源码是怎么写的吧,这里打开lib/core/Axios.js

// Axios构造函数,定义l额defaults属性和interceptors属性
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

Axios.prototype.request = function request(config) {
  // 这里对传入参数类型的判断,如果传入第一参数为字符串,
  // 则认为字符串是url,并且把url参数添加到第二个参数config中
  // 所以就实现了第二种调用方法axios('example/url'[, config])
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // ...省略了一些代码
  }

// 这里对Axios原型扩展了'delete', 'get', 'head', 'options'方法,
// 其实都是调用了request方法
// 结合上面lib / axios.js 代码中把原型中方法扩展到了instance上
// 所以第四种方法axios.get(url[, config])就实现了
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url
    }));
  };
});

// 这里对Axios原型扩展了'post', 'put', 'patch'方法,
// 其实都是调用了request方法
// 结合上面lib / axios.js 代码中把原型中方法扩展到了instance上
// 所以第五种方法axios.post(url[, data[, config]])就实现了
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(mergeConfig(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

五、注册拦截器

前面解析了axios对外接口是怎么实现的,它们共同调用了request方法。下面是不是应该解析一下request方法呢。先别急,request方法内部是需要处理拦截器的,所以我们先看看拦截器是怎么注册的,原理是什么。
先看看怎么注册

// 添加请求拦截器
const myRequestInterceptor = axios.interceptors.request.use(config => {
    // 在发送http请求之前对config做点什么
    return config; // 必须返回config否则后续http请求无法获取到config
}, error => {
    // 错误处理代码写在这里
    
    // 但是,为什么返回Promise.reject(error)
    // 这和promise的机制有关
    // 如果直接抛出error就相当于抛出一个对象,
    // 这就会运行下一级promise的fulfilled方法。并且参数是error,这个fulfilled参数应该是config才对
    // 或者运行到dispatchRequest,参数error,但dispatchRequest参数也要是config才对
    return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(response => {
  // 对响应数据处理代码写这里
  return response; // 有且必须有一个response对象被返回
}, error => {
  // 对响应错误处理代码写这里
  
  // 同理请求拦截器,这需要返回Promise.reject(error);
  return Promise.reject(error);
});

// 移除某次拦截器
axios.interceptors.request.eject(myRequestInterceptor);

前面提到过在lib / axios.js这里已经将context对象的属性扩展到了instance,也就是对外接口axios。所以axios就有了interceptor属性,里面有两个属性request和response,都是InterceptorManager对象,目的是存储注册的拦截器。

那么InterceptorManager都做了什么呢,看源码:

// 构造函数,在对象中定义handlers用来存储拦截器
function InterceptorManager() {
  this.handlers = [];
}

/**
 * 注册拦截器,并存储在handlers中
 * 参数fulfilled,用来拦截器处理数据的函数
 * 参数rejected,用来处理错误用的
 * 为什么这么设计,因为拦截器要通过Promise处理
 * 返回本条拦截器在数组handlers中的索引位置,以便提供给删除拦截器用
 */
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

/**
 * 通过注册时候返回的拦截器索引来删除拦截器
 */
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
/**
 * 遍历拦截器的方法
 */
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};

注册完成了,那么怎么发挥作用呢,那就要看看request方法了。

六、request方法,建立Promise链

request方法是Axios的核心方法。接下来我们看看方法都实现了什么。打开lib / core / axios.js,我们看一下主要代码:

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

Axios.prototype.request = function request(config) {
  // 刚进入方法,首先要处理config这里存放着http请求的必要配置
  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';
  }
  // 到这里处理完用于http请求的配置数据config
  
  // 先定义个数组,先放入一个dispatchRequest和undefined,
  // dispatchRequest前面的项目目录介绍里提到了,用来申请http请求用的
  // 为什么先建立个数组呢,作者的目的就是想先把拦截器和http请求先排好序
  // 然后再建立promise调用链,就可以一步一步的按顺序进行了
  // 入果没有理解,建议先深入研究一下Promise链式调用
  // 为什么先放dispatchRequest,又放个undefined呢
  // 可以先看一下下面怎么向chain插入拦截器的
  // 拦截器被插入到数组,并且一次向数组插入两个方法,interceptor.fulfilled, interceptor.rejected
  // 再看看后面建立promise链式调用的时候,分别用在了then的两个参数,是从数组中一起取两个的
  // 所以为了保证拦截器两个方法配对正确所以先插入[dispatchRequest, undefined]
  // 之所以用undefined,因为这里没法处理错综复杂而且多变的错误。而且这里也只能用来处理请求拦截器的错误。所以没有必要。
  // 所以用undefined,把错误抛到下面的promise,由用户定义处理方法。
  var chain = [dispatchRequest, undefined];

  // 先初始一个promise,value是config,
  // 提供给下面的promise用,也就是提供给请求拦截器用
  var promise = Promise.resolve(config);

  // 向chain数组插入请求拦截器。一对一对的插入
  // 注意这里是从前插入请求拦截器的
  // 所以用的时候,先注册的请求拦截器是后执行的,这点要注意
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // 向chain数组插入相应拦截器。一对一对的插入
  // 相应拦截器是从后插入的
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  // 到此,把拦截器和http请求拍好顺序了
  // 下面就利用这个循序建立一个promise链
  // promise链让拦截器和http请求按照顺序执行了,执行顺序是:
  // 请求拦截器->http请求->相应拦截器
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

到这里,已经建立了一个promise链。先执行请求拦截器,按照需求修改config。然后执行http请求。请求结果,传到相应拦截器处理。最后抛出链调用最后的promise,我们用这个promise就能得到这一串处理的最终结果了。

七、数据修改器

到这里貌似核心功能流程都有了,可以跑通了。但是,我们回忆一下,之前还提到了数据修改器,用来修改请求和相应的数据用的。这是怎么回事儿呢。我们先看一下如何注册修改器。

import axios from 'axios'

// 往现有的请求转换器里增加转换方法
axios.defaults.transformRequest.push((data, headers) => {
  // ...处理data
  return data;
});

// 重写请求转换器
axios.defaults.transformRequest = [(data, headers) => {
  // ...处理data
  return data;
}];

// 往现有的响应转换器里增加转换方法
axios.defaults.transformResponse.push((data, headers) => {
  // ...处理data
  return data;
});

// 重写响应转换器
axios.defaults.transformResponse = [(data, headers) => {
  // ...处理data
  return data;
}];

// 修改某次axios请求的转换器
// // 往已经存在的转换器里增加转换方法
axios.get(url, {
  // ...
  transformRequest: [
    ...axios.defaults.transformRequest, // 去掉这行代码就等于重写请求转换器了
    (data, headers) => {
      // ...处理data
      return data;
    }
  ],
  transformResponse: [
    ...axios.defaults.transformResponse, // 去掉这行代码就等于重写响应转换器了
    (data, headers) => {
      // ...处理data
      return data;
    }
  ],
})

上面的数据修改器注册的方法就是在向config中的(defaults就是默认的config)transformRequest或transformResponse插入方法或者直接重新定义修改器数组

我们来看看默认配置lib / defaults.js里关于数据修改去都怎么定义的吧:

var defaults = {
  transformRequest: [function transformRequest(data, headers) {
   // 省略了代码
   // 修改之后要返回修改之后的data,因为需要重新给data赋值,
   // 不用返回headers,因为headers是对象,修改对象本身,对象就改变,
   // data有可能不是对象,修改之后要重新复制给config.data
   return data;
  }],

  transformResponse: [function transformResponse(data) {
    // 省略了代码
    // 返回data,因为response.data不一定是对象。所以修改后要重新复制
    return data;
  }],
};

在默认配置里,已经定义了数据修改器,所以我们要想添加新的修改器就push新的就行,也可以重新替换掉这个数组,自己重新定义

八、dispatchRequest解析-数据修改器执行原理

定义数据修改器了。怎么起作用呢?上面已经说了,在dispatchRequest中处理。上面提到的promise链中,运行好请求拦截器后,就会调用dispatchRequset,并且传入config配置作为参数。下面开lib/core/dispatchRequest.js看一下,里面是如何处理数据修改器和调用http请求的。

module.exports = function dispatchRequest(config) {
  // 执行请求数据修改器
  // 这里重新给config.data赋值
  // 所以定义数据修改器的时候要返回data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
  
  // 这里是获取http请求方法的
  var adapter = config.adapter || defaults.adapter;
  // adapter(config)是进行http请求
  // 方法返回promise对象,将这个promise作为dispatchRequest返回值,
  // 用于后面的相应拦截器处理
  return adapter(config).then(function onAdapterResolution(response) {
    // 请求成功,执行响应数据修改器
    // 这里要给response.data重新赋值,所以定义响应数据修改器要返回data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

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

      // 请求有误,也执行响应数据修改器
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

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

上面源码看到在http请求之前,执行了transformRequest请求数据修改器,在请求之后无论http请求结果成功还是失败,都执行了transformResponse请求数据修改器,都是放在transformData方法中执行的

transformData方法内部怎么实现的我们看一下。

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

  return data;
};

看一下很简单,就是遍历数据修改器,把data和headers为参数传给定义的数据修改器处理,处理的结果返回给data,交由下一个修改器处理,直到修改器都执行完,最后返回处理好的data。

九、拦截器和数据修改器的区别

看到这里你是不是有个疑问。之前我们定义的请求拦截器就能修改整个congfig,响应拦截器能修改整个response,请求数据修改器只能修改config.data和config.headers,响应数据修改器只能修改response.data。何必又修改器的功能呢?

根据源码可以得出以下几点,这里是个人的一点儿分析:

第一,看拦截器和数据修改器的参数,明显拦截器的范围更大一些。所以拦截器侧重整体配置的修改。而数据修改器,侧重于对操作的数据进行修改。数据修改器和应用程序的耦合度更高。

第二,拦截器是在Axios构造函数中定义的,是axios对象的属性,在config中不能配置。而数据修改器是在config中定义的。

这能说明什么?

先看数据修改器,可以在axios请求的时候配置在congfig中,在执行Request方法开始的时候就把默认的修改器覆盖成自定义的修改器,不会修改默认配置,下次axios请求的时候如果没有配置修改器,还是用默认的修改器。这样就可以在每一次axios请求中都配置不同的修改器,每次axios请求互补干扰,而且每次axios请求中配置config即可,一条语句解决问题,不需要多于的语句。

而拦截器就不同了,没有这么灵活,一次注册只要不主动删除每次axios请求都会用到注册的拦截器,这就强调了拦截器全局定义的特性。如果想每次axios请求的拦截器都有变化,就会很不方便,而且难免处理不当,造成对其他axios请求的干扰。

当然,数据修改器也可以全局设定,就是修改defaults的数据修改器

总结:

一、拦截器用户配置级别的修改,侧重整体配置层面。数据修改器用于http请求和响应时数据的修改,侧重于应用的数据层面。

二、拦截器侧重全局配置,一次配置全局使用。数据修改器更加灵活,可以全局配置,一次配置全局使用,也可以每次axios请求配置不同的修改器。


十、http请求适配器,自定义http请求适配器

在dispatchRequest中怎么执行修改器搞清楚了,上面的dispatchRequest源码里还有选择http请求适配器并执行请求的过程。这块也给axios的使用者提供了可扩展的方式。有必要提一下

源码中定义适配器是这样的:

// 这里是获取http请求方法的
// 先判断config中知否有自定义适配器,如果没有则调用默认的适配器
  var adapter = config.adapter || defaults.adapter;

先判断config中知否有自定义适配器,如果没有则调用默认的适配器。

所以我们可以通过定义config的adapter属性定义自己的http请求。可以每次申请的时候定义,也可以通过拦截器定义全局的http请求适配器。这里不做扩展,大家可以思考一下。

默认适配器怎么调用,看看defaults.adapter源码什么样,打开lib/defaults.js

// getDefaultAdapter方法通过判断是浏览器还是node环境选择相应的适配器
// node环境用./adapters/http
// 浏览器环境用./adapters/xhr
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 = {
  // 默认用getDefaultAdapter方法选择适配器
  adapter: getDefaultAdapter(),
}

源码中默认给出了两套http请求适配器,node环境下用http适配器,浏览器环境用xhr适配器。具体源码是lib/adapters/文件夹中的http.js和xhr.js。这里不做具体分析,很简单。都是很独立的功能。

运行完dispatchRequest之后就是运行响应拦截器,最后返回Promise链,得到一个Promise。这里就有我们需要的结果,之后处理。上面的六、request方法,建立Promise链详中详细分析了,可以回看

到此整个核心框架、流程就都有了:入口》响应拦截器》响应数据修改器》http请求》响应数据修改器》响应拦截器》返回Promise

总结了一个流程图帮助大家理解记忆:


十一、终止Axios请求

到此已经搞清楚axios的核心流程了,看起来很长的过程。如果中间因为某种原因我们不需要这个请求。想终止Axios请求怎么办呢。有人说了,http请求不是有abort()方法么,直接调用adapters里的abort()方法不就结了。
看看上面总结的Axios流程那么老长一串儿,这样的终止方法是不是太草率了。如果我在执行上面流程某一阶段的时候发现不太对劲儿,想就在这停止流程了怎么办,向下执行没有意义,还有可能导致不必要的错误。
所以作者在每个过程中都埋下终止的控制器就可以及时终止axios的申请。
Axios提供了这样的终止方法。
还是要先看看如何使用:

// 第一种取消方法
axios.get(url, {
  cancelToken: new axios.CancelToken(cancel => {
    if (/* 取消条件 */) {
      cancel('取消日志');
    }
  })
});

// 第二种取消方法
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get(url, {
  cancelToken: source.token
});
source.cancel('取消日志');

咦!整这么复杂干啥,直接在axios实例里设置个开关,true为继续,false为终止,流程里每一步判断一下不就行了么。这样想就有点考虑简单了。

先看看源码,都在哪里埋下终止的控制器了,详细解释请看代码里的注释

lib/core/dispatchRequest.js

// 这个是判断是否停止的同步方法
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}

module.exports = function dispatchRequest(config) {
  // 进入dispatchRequest方法要判断一下,
  // 这个时候刚执行完请求拦截器
  throwIfCancellationRequested(config);

  // 然后执行请求数据修改器
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  var adapter = config.adapter || defaults.adapter;
 // 接下来http请求。
  return adapter(config).then(function onAdapterResolution(response) {
    // http请求成功,服务器返回之后判断一下,这时候还没执行响应数据修改器
    throwIfCancellationRequested(config);

    // 执行响应数据修改器
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      // http请求出问题,并且判断是用户终止axios的问题,
      // 判断是否终止
      throwIfCancellationRequested(config);

      // 执行响应数据修改器,
      // 这个时候有可能数据已经返回了所以还是要处理一下数
      //  因为有可能用到,这要看用户自己处理了,提供了处理数据,备用
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });

所以在执行数据修改器之前,都埋了终止控制器throwIfCancellationRequested

lib/adapters/xhr.js

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {

    var request = new XMLHttpRequest();
    // 省略了很多无关代码


    // 判断是否设置了cancelToken
    if (config.cancelToken) {
      // 设置了cancelToken
      // cancelToken有promise属性,promise状态变为fulfilled说明需要终止
      // 这是异步执行的判断呦,即使现在是终止状态,
      // 下面的request.send也会执行,
      // 为啥不像dispatchRequest中直接同步判断,
      // 如果终止 后面的request.send不执行,不就起到终止的效果了么
      // 为啥要这样,即使终止了也要send出去
      // 原因是这样的
      // 作者这样处理的目的不是要阻止request.send
      // 而是要控制从request.send到服务器返回结果,这个阶段的终止
      // 你细品,细品,品
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

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

    // Send the request
    request.send(requestData);
  });
};

 lib/adapters/http.js 原理和lib/adapters/xhr.js 一样。

所以总结终止方案的需求:

一、是个容器,有状态,是否终止的状态

二、能同步判断知否终止,也能异步判断知否终止

三、在axios文档里有一句话(注意: 可以使用同一个 cancel token 取消多个请求)所以要满足这个注意事项。

所以就单独出一个cancelToken对象,axios内部识别cancelToken对象里面的状态,从而控制请求终止。

看看源码lib/cancel/CancelToken.js怎么实现的吧

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

// 定义一个promise属性,把resolve拿出来备用
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;

  // 把控制状态的方法cancel传给回调函数
  // 这样外部就可以控制改变CancelToken的状态了
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

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

/**
 * 这是个语法糖,可以直接调用就可以返回CancelToken对象
 * 和控制状态的方法cancel
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

总结了一个CancelToken在这个axios流程中的作用的路程图,希望帮助大家理解:


以上就是个人通过对axios源码阅读之后总结出来的,希望能给大家有所帮助,也希望能得到指点和吐槽。