你了解axios的原理吗?有看过它的源码吗? - 图1

一、axios的使用

关于axios的基本使用,上篇文章已经有所涉及,这里再稍微回顾下:

发送请求

  1. import axios from 'axios';
  2. axios(config) // 直接传入配置
  3. axios(url[, config]) // 传入url和配置
  4. axios[method](url[, option]) // 直接调用请求方式方法,传入url和配置
  5. axios[method](url[, data[, option]]) // 直接调用请求方式方法,传入data、url和配置
  6. axios.request(option) // 调用 request 方法
  7. const axiosInstance = axios.create(config)
  8. // axiosInstance 也具有以上 axios 的能力
  9. axios.all([axiosInstance1, axiosInstance2]).then(axios.spread(response1, response2))
  10. // 调用 all 和传入 spread 回调

请求拦截器

  1. axios.interceptors.request.use(function (config) {
  2. // 这里写发送请求前处理的代码
  3. return config;
  4. }, function (error) {
  5. // 这里写发送请求错误相关的代码
  6. return Promise.reject(error);
  7. });

响应拦截器

  1. axios.interceptors.response.use(function (response) {
  2. // 这里写得到响应数据后处理的代码
  3. return response;
  4. }, function (error) {
  5. // 这里写得到错误响应处理的代码
  6. return Promise.reject(error);
  7. });

取消请求

  1. // 方式一
  2. const CancelToken = axios.CancelToken;
  3. const source = CancelToken.source();
  4. axios.get('xxxx', {
  5. cancelToken: source.token
  6. })
  7. // 取消请求 (请求原因是可选的)
  8. source.cancel('主动取消请求');
  9. // 方式二
  10. const CancelToken = axios.CancelToken;
  11. let cancel;
  12. axios.get('xxxx', {
  13. cancelToken: new CancelToken(function executor(c) {
  14. cancel = c;
  15. })
  16. });
  17. cancel('主动取消请求');

二、实现一个简易版axios

构建一个Axios构造函数,核心代码为request

  1. class Axios {
  2. constructor() {
  3. }
  4. request(config) {
  5. return new Promise(resolve => {
  6. const {url = '', method = 'get', data = {}} = config;
  7. // 发送ajax请求
  8. const xhr = new XMLHttpRequest();
  9. xhr.open(method, url, true);
  10. xhr.onload = function() {
  11. console.log(xhr.responseText)
  12. resolve(xhr.responseText);
  13. }
  14. xhr.send(data);
  15. })
  16. }
  17. }

导出axios实例

  1. // 最终导出axios的方法,即实例的request方法
  2. function CreateAxiosFn() {
  3. let axios = new Axios();
  4. let req = axios.request.bind(axios);
  5. return req;
  6. }
  7. // 得到最后的全局变量axios
  8. let axios = CreateAxiosFn();

上述就已经能够实现axios({ })这种方式的请求

下面是来实现下axios.method()这种形式的请求

  1. // 定义get,post...方法,挂在到Axios原型上
  2. const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
  3. methodsArr.forEach(met => {
  4. Axios.prototype[met] = function() {
  5. console.log('执行'+met+'方法');
  6. // 处理单个方法
  7. if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config])
  8. return this.request({
  9. method: met,
  10. url: arguments[0],
  11. ...arguments[1] || {}
  12. })
  13. } else { // 3个参数(url[,data[,config]])
  14. return this.request({
  15. method: met,
  16. url: arguments[0],
  17. data: arguments[1] || {},
  18. ...arguments[2] || {}
  19. })
  20. }
  21. }
  22. })

Axios.prototype上的方法搬运到request

首先实现个工具类,实现将b方法混入到a,并且修改this指向

  1. const utils = {
  2. extend(a,b, context) {
  3. for(let key in b) {
  4. if (b.hasOwnProperty(key)) {
  5. if (typeof b[key] === 'function') {
  6. a[key] = b[key].bind(context);
  7. } else {
  8. a[key] = b[key]
  9. }
  10. }
  11. }
  12. }
  13. }

修改导出的方法

  1. function CreateAxiosFn() {
  2. let axios = new Axios();
  3. let req = axios.request.bind(axios);
  4. // 增加代码
  5. utils.extend(req, Axios.prototype, axios)
  6. return req;
  7. }

构建拦截器的构造函数

  1. class InterceptorsManage {
  2. constructor() {
  3. this.handlers = [];
  4. }
  5. use(fullfield, rejected) {
  6. this.handlers.push({
  7. fullfield,
  8. rejected
  9. })
  10. }
  11. }

实现axios.interceptors.response.useaxios.interceptors.request.use

  1. class Axios {
  2. constructor() {
  3. // 新增代码
  4. this.interceptors = {
  5. request: new InterceptorsManage,
  6. response: new InterceptorsManage
  7. }
  8. }
  9. request(config) {
  10. ...
  11. }
  12. }

执行语句axios.interceptors.response.useaxios.interceptors.request.use的时候,实现获取axios实例上的interceptors对象,然后再获取responserequest拦截器,再执行对应的拦截器的use方法

Axios上的方法和属性搬到request过去

  1. function CreateAxiosFn() {
  2. let axios = new Axios();
  3. let req = axios.request.bind(axios);
  4. // 混入方法, 处理axios的request方法,使之拥有get,post...方法
  5. utils.extend(req, Axios.prototype, axios)
  6. // 新增代码
  7. utils.extend(req, axios)
  8. return req;
  9. }

现在request也有了interceptors对象,在发送请求的时候,会先获取request拦截器的handlers的方法来执行

首先将执行ajax的请求封装成一个方法

  1. request(config) {
  2. this.sendAjax(config)
  3. }
  4. sendAjax(config){
  5. return new Promise(resolve => {
  6. const {url = '', method = 'get', data = {}} = config;
  7. // 发送ajax请求
  8. console.log(config);
  9. const xhr = new XMLHttpRequest();
  10. xhr.open(method, url, true);
  11. xhr.onload = function() {
  12. console.log(xhr.responseText)
  13. resolve(xhr.responseText);
  14. };
  15. xhr.send(data);
  16. })
  17. }

获得handlers中的回调

  1. request(config) {
  2. // 拦截器和请求组装队列
  3. let chain = [this.sendAjax.bind(this), undefined] // 成对出现的,失败回调暂时不处理
  4. // 请求拦截
  5. this.interceptors.request.handlers.forEach(interceptor => {
  6. chain.unshift(interceptor.fullfield, interceptor.rejected)
  7. })
  8. // 响应拦截
  9. this.interceptors.response.handlers.forEach(interceptor => {
  10. chain.push(interceptor.fullfield, interceptor.rejected)
  11. })
  12. // 执行队列,每次执行一对,并给promise赋最新的值
  13. let promise = Promise.resolve(config);
  14. while(chain.length > 0) {
  15. promise = promise.then(chain.shift(), chain.shift())
  16. }
  17. return promise;
  18. }

chains大概是['fulfilled1','reject1','fulfilled2','reject2','this.sendAjax','undefined','fulfilled2','reject2','fulfilled1','reject1']这种形式

这样就能够成功实现一个简易版axios

三、源码分析

首先看看目录结构

你了解axios的原理吗?有看过它的源码吗? - 图2

axios发送请求有很多实现的方法,实现入口文件为axios.js

  1. function createInstance(defaultConfig) {
  2. var context = new Axios(defaultConfig);
  3. // instance指向了request方法,且上下文指向context,所以可以直接以 instance(option) 方式调用
  4. // Axios.prototype.request 内对第一个参数的数据类型判断,使我们能够以 instance(url, option) 方式调用
  5. var instance = bind(Axios.prototype.request, context);
  6. // 把Axios.prototype上的方法扩展到instance对象上,
  7. // 并指定上下文为context,这样执行Axios原型链上的方法时,this会指向context
  8. utils.extend(instance, Axios.prototype, context);
  9. // Copy context to instance
  10. // 把context对象上的自身属性和方法扩展到instance上
  11. // 注:因为extend内部使用的forEach方法对对象做for in 遍历时,只遍历对象本身的属性,而不会遍历原型链上的属性
  12. // 这样,instance 就有了 defaults、interceptors 属性。
  13. utils.extend(instance, context);
  14. return instance;
  15. }
  16. // Create the default instance to be exported 创建一个由默认配置生成的axios实例
  17. var axios = createInstance(defaults);
  18. // Factory for creating new instances 扩展axios.create工厂函数,内部也是 createInstance
  19. axios.create = function create(instanceConfig) {
  20. return createInstance(mergeConfig(axios.defaults, instanceConfig));
  21. };
  22. // Expose all/spread
  23. axios.all = function all(promises) {
  24. return Promise.all(promises);
  25. };
  26. axios.spread = function spread(callback) {
  27. return function wrap(arr) {
  28. return callback.apply(null, arr);
  29. };
  30. };
  31. module.exports = axios;

主要核心是 Axios.prototype.request,各种请求方式的调用实现都是在 request 内部实现的, 简单看下 request 的逻辑

  1. Axios.prototype.request = function request(config) {
  2. // Allow for axios('example/url'[, config]) a la fetch API
  3. // 判断 config 参数是否是 字符串,如果是则认为第一个参数是 URL,第二个参数是真正的config
  4. if (typeof config === 'string') {
  5. config = arguments[1] || {};
  6. // 把 url 放置到 config 对象中,便于之后的 mergeConfig
  7. config.url = arguments[0];
  8. } else {
  9. // 如果 config 参数是否是 字符串,则整体都当做config
  10. config = config || {};
  11. }
  12. // 合并默认配置和传入的配置
  13. config = mergeConfig(this.defaults, config);
  14. // 设置请求方法
  15. config.method = config.method ? config.method.toLowerCase() : 'get';
  16. /*
  17. something... 此部分会在后续拦截器单独讲述
  18. */
  19. };
  20. // 在 Axios 原型上挂载 'delete', 'get', 'head', 'options' 且不传参的请求方法,实现内部也是 request
  21. utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  22. Axios.prototype[method] = function(url, config) {
  23. return this.request(utils.merge(config || {}, {
  24. method: method,
  25. url: url
  26. }));
  27. };
  28. });
  29. // 在 Axios 原型上挂载 'post', 'put', 'patch' 且传参的请求方法,实现内部同样也是 request
  30. utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  31. Axios.prototype[method] = function(url, data, config) {
  32. return this.request(utils.merge(config || {}, {
  33. method: method,
  34. url: url,
  35. data: data
  36. }));
  37. };
  38. });

request入口参数为config,可以说config贯彻了axios的一生

axios 中的 config主要分布在这几个地方:

  • 默认配置 defaults.js
  • config.method默认为 get
  • 调用 createInstance 方法创建 axios实例,传入的config
  • 直接或间接调用 request 方法,传入的 config
  1. // axios.js
  2. // 创建一个由默认配置生成的axios实例
  3. var axios = createInstance(defaults);
  4. // 扩展axios.create工厂函数,内部也是 createInstance
  5. axios.create = function create(instanceConfig) {
  6. return createInstance(mergeConfig(axios.defaults, instanceConfig));
  7. };
  8. // Axios.js
  9. // 合并默认配置和传入的配置
  10. config = mergeConfig(this.defaults, config);
  11. // 设置请求方法
  12. config.method = config.method ? config.method.toLowerCase() : 'get';

从源码中,可以看到优先级:默认配置对象default < method:get < Axios的实例属性this.default < request参数

下面重点看看request方法

  1. Axios.prototype.request = function request(config) {
  2. /*
  3. 先是 mergeConfig ... 等,不再阐述
  4. */
  5. // Hook up interceptors middleware 创建拦截器链. dispatchRequest 是重中之重,后续重点
  6. var chain = [dispatchRequest, undefined];
  7. // push各个拦截器方法 注意:interceptor.fulfilled 或 interceptor.rejected 是可能为undefined
  8. this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
  9. // 请求拦截器逆序 注意此处的 forEach 是自定义的拦截器的forEach方法
  10. chain.unshift(interceptor.fulfilled, interceptor.rejected);
  11. });
  12. this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  13. // 响应拦截器顺序 注意此处的 forEach 是自定义的拦截器的forEach方法
  14. chain.push(interceptor.fulfilled, interceptor.rejected);
  15. });
  16. // 初始化一个promise对象,状态为resolved,接收到的参数为已经处理合并过的config对象
  17. var promise = Promise.resolve(config);
  18. // 循环拦截器的链
  19. while (chain.length) {
  20. promise = promise.then(chain.shift(), chain.shift()); // 每一次向外弹出拦截器
  21. }
  22. // 返回 promise
  23. return promise;
  24. };

拦截器interceptors是在构建axios实例化的属性

  1. function Axios(instanceConfig) {
  2. this.defaults = instanceConfig;
  3. this.interceptors = {
  4. request: new InterceptorManager(), // 请求拦截
  5. response: new InterceptorManager() // 响应拦截
  6. };
  7. }

InterceptorManager构造函数

  1. // 拦截器的初始化 其实就是一组钩子函数
  2. function InterceptorManager() {
  3. this.handlers = [];
  4. }
  5. // 调用拦截器实例的use时就是往钩子函数中push方法
  6. InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  7. this.handlers.push({
  8. fulfilled: fulfilled,
  9. rejected: rejected
  10. });
  11. return this.handlers.length - 1;
  12. };
  13. // 拦截器是可以取消的,根据use的时候返回的ID,把某一个拦截器方法置为null
  14. // 不能用 splice 或者 slice 的原因是 删除之后 id 就会变化,导致之后的顺序或者是操作不可控
  15. InterceptorManager.prototype.eject = function eject(id) {
  16. if (this.handlers[id]) {
  17. this.handlers[id] = null;
  18. }
  19. };
  20. // 这就是在 Axios的request方法中 中循环拦截器的方法 forEach 循环执行钩子函数
  21. InterceptorManager.prototype.forEach = function forEach(fn) {
  22. utils.forEach(this.handlers, function forEachHandler(h) {
  23. if (h !== null) {
  24. fn(h);
  25. }
  26. });
  27. }

请求拦截器方法是被 unshift到拦截器中,响应拦截器是被push到拦截器中的。最终它们会拼接上一个叫dispatchRequest的方法被后续的 promise 顺序执行

  1. var utils = require('./../utils');
  2. var transformData = require('./transformData');
  3. var isCancel = require('../cancel/isCancel');
  4. var defaults = require('../defaults');
  5. var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
  6. var combineURLs = require('./../helpers/combineURLs');
  7. // 判断请求是否已被取消,如果已经被取消,抛出已取消
  8. function throwIfCancellationRequested(config) {
  9. if (config.cancelToken) {
  10. config.cancelToken.throwIfRequested();
  11. }
  12. }
  13. module.exports = function dispatchRequest(config) {
  14. throwIfCancellationRequested(config);
  15. // 如果包含baseUrl, 并且不是config.url绝对路径,组合baseUrl以及config.url
  16. if (config.baseURL && !isAbsoluteURL(config.url)) {
  17. // 组合baseURL与url形成完整的请求路径
  18. config.url = combineURLs(config.baseURL, config.url);
  19. }
  20. config.headers = config.headers || {};
  21. // 使用/lib/defaults.js中的transformRequest方法,对config.headers和config.data进行格式化
  22. // 比如将headers中的Accept,Content-Type统一处理成大写
  23. // 比如如果请求正文是一个Object会格式化为JSON字符串,并添加application/json;charset=utf-8的Content-Type
  24. // 等一系列操作
  25. config.data = transformData(
  26. config.data,
  27. config.headers,
  28. config.transformRequest
  29. );
  30. // 合并不同配置的headers,config.headers的配置优先级更高
  31. config.headers = utils.merge(
  32. config.headers.common || {},
  33. config.headers[config.method] || {},
  34. config.headers || {}
  35. );
  36. // 删除headers中的method属性
  37. utils.forEach(
  38. ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
  39. function cleanHeaderConfig(method) {
  40. delete config.headers[method];
  41. }
  42. );
  43. // 如果config配置了adapter,使用config中配置adapter的替代默认的请求方法
  44. var adapter = config.adapter || defaults.adapter;
  45. // 使用adapter方法发起请求(adapter根据浏览器环境或者Node环境会有不同)
  46. return adapter(config).then(
  47. // 请求正确返回的回调
  48. function onAdapterResolution(response) {
  49. // 判断是否以及取消了请求,如果取消了请求抛出以取消
  50. throwIfCancellationRequested(config);
  51. // 使用/lib/defaults.js中的transformResponse方法,对服务器返回的数据进行格式化
  52. // 例如,使用JSON.parse对响应正文进行解析
  53. response.data = transformData(
  54. response.data,
  55. response.headers,
  56. config.transformResponse
  57. );
  58. return response;
  59. },
  60. // 请求失败的回调
  61. function onAdapterRejection(reason) {
  62. if (!isCancel(reason)) {
  63. throwIfCancellationRequested(config);
  64. if (reason && reason.response) {
  65. reason.response.data = transformData(
  66. reason.response.data,
  67. reason.response.headers,
  68. config.transformResponse
  69. );
  70. }
  71. }
  72. return Promise.reject(reason);
  73. }
  74. );
  75. };

再来看看axios是如何实现取消请求的,实现文件在CancelToken.js

  1. function CancelToken(executor) {
  2. if (typeof executor !== 'function') {
  3. throw new TypeError('executor must be a function.');
  4. }
  5. // 在 CancelToken 上定义一个 pending 状态的 promise ,将 resolve 回调赋值给外部变量 resolvePromise
  6. var resolvePromise;
  7. this.promise = new Promise(function promiseExecutor(resolve) {
  8. resolvePromise = resolve;
  9. });
  10. var token = this;
  11. // 立即执行 传入的 executor函数,将真实的 cancel 方法通过参数传递出去。
  12. // 一旦调用就执行 resolvePromise 即前面的 promise 的 resolve,就更改promise的状态为 resolve。
  13. // 那么xhr中定义的 CancelToken.promise.then方法就会执行, 从而xhr内部会取消请求
  14. executor(function cancel(message) {
  15. // 判断请求是否已经取消过,避免多次执行
  16. if (token.reason) {
  17. return;
  18. }
  19. token.reason = new Cancel(message);
  20. resolvePromise(token.reason);
  21. });
  22. }
  23. CancelToken.source = function source() {
  24. // source 方法就是返回了一个 CancelToken 实例,与直接使用 new CancelToken 是一样的操作
  25. var cancel;
  26. var token = new CancelToken(function executor(c) {
  27. cancel = c;
  28. });
  29. // 返回创建的 CancelToken 实例以及取消方法
  30. return {
  31. token: token,
  32. cancel: cancel
  33. };
  34. };

实际上取消请求的操作是在 xhr.js 中也有响应的配合的

  1. if (config.cancelToken) {
  2. config.cancelToken.promise.then(function onCanceled(cancel) {
  3. if (!request) {
  4. return;
  5. }
  6. // 取消请求
  7. request.abort();
  8. reject(cancel);
  9. });
  10. }

巧妙的地方在 CancelTokenexecutor 函数,通过resolve函数的传递与执行,控制promise的状态

小结

你了解axios的原理吗?有看过它的源码吗? - 图3

参考文献