前言

为什么会想去读源码?

起因还是写小程序项目的时候,在回调里面总是写很多相同的判断,大大降低了我的码代码体验感???

axios自带拦截器,等等很多有用的方法,一直听闻axios实现很巧妙,虽然网络上已经有很对大佬的解读过axios,对我来说看文章不够尽兴,只有亲自探究axios的内部实现?

从入口开始

下载了axios的源码

  1. git clone https://github.com/axios/axios.git
  2. 复制代码

就在我想这个源文件怎么运行的时候,我发现在文件夹里面似乎有示例代码sandbox里面有示例代码

运行后发现,这个是打包过的代码(陷入沉思.....)

因为浏览器不识别require() 此类的语法,所以直接应用源码会报错

于是

搭建一个查看axios源码的环境

也就是支持es6的webpack配置(打包工具都可以,我对webpack比较熟悉,所以使用了wekpack)

这里解决的就是浏览器不认识require的问题,所以我们使用webpack,来搭建一个支持es6的开发环境,(还支持热加载,美滋滋~~~)

目录结构

这里我引用自知乎深入浅出 axios 源码,目录结构写的清楚明了,前人栽树后人乘凉,我便直接引用了

  1. ├── /lib/ # 项目源码目
  2. │ ├── /adapters/ # 定义发送请求的适配器
  3. │ │ ├── http.js # node环境http对象
  4. │ │ └── xhr.js # 浏览器环境XML对象
  5. │ ├── /cancel/ # 定义取消功能
  6. │ ├── /helpers/ # 一些辅助方法
  7. │ ├── /core/ # 一些核心功能
  8. │ │ ├── Axios.js # axios实例构造函数
  9. │ │ ├── createError.js # 抛出错误
  10. │ │ ├── dispatchRequest.js # 用来调用http请求适配器方法发送请求
  11. │ │ ├── InterceptorManager.js # 拦截器管理器
  12. │ │ ├── mergeConfig.js # 合并参数
  13. │ │ ├── settle.js # 根据http响应状态,改变Promise的状态
  14. │ │ └── transformData.js # 改变数据格式
  15. │ ├── axios.js # 入口,创建构造函数
  16. │ ├── defaults.js # 默认配置
  17. │ └── utils.js # 公用工具
  18. 复制代码

axios的运行流程

我阅读完成源码后,梳理了一下运行思路,axios大致分为 四个阶段

  1. 初始化参数
  2. 初始化拦截器,添加原型链方法
  3. 合并原型链对象到axios,便于直接调用
  4. 开始进行请求(有拦截器执行拦截器)

下面将结合代码以及图片进行axios内部实现过程的梳理

当我们开始使用axios的时候,例如

  1. let axios = require('./lib/axios')
  2. axios('https://www.easy-mock.com/mock/.....')
  3. .then(res => {
  4. // .....
  5. })
  6. .catch(res => {
  7. // .....
  8. })
  9. 复制代码

使用了./lib/axios导出的axios对象,毫无疑问这是axios的入口文件

初始化参数

lib/axios

为了便于查看,我删除了一些axios其他方法的代码

  1. 'use strict'
  2. var utils = require('./utils')
  3. var bind = require('./helpers/bind')
  4. var Axios = require('./core/Axios')
  5. var mergeConfig = require('./core/mergeConfig')
  6. var defaults = require('./defaults')
  7. /**
  8. * 创建Axios实例
  9. *
  10. * @param {Object} defaultConfig实例的默认配置
  11. * @return {Axios} Axios的新实例
  12. */
  13. function createInstance(defaultConfig) { // (2)
  14. // defaultConfig 为初始化配置
  15. var context = new Axios(defaultConfig);
  16. // bind(Axios.prototype.request, context)的目的是让axios默认及request方法
  17. var instance = bind(Axios.prototype.request, context)
  18. // 将Axios.prototype的原型链上的对象复制到instance上面
  19. utils.extend(instance, Axios.prototype, context);
  20. // 将拦截器,默认配置复制到instance(这里主要是拦截器操作)
  21. utils.extend(instance, context)
  22. return instance
  23. }
  24. // 创建要导出的默认实例
  25. // 当axios执行的时候 首先axios进行默认的初始化配置
  26. var axios = createInstance(defaults) // (1)
  27. module.exports = axios
  28. // 允许在TypeScript中使用默认导入语法
  29. module.exports.default = axios
  30. 复制代码

这里可以看到,当我们执行axios的时候,axios =createInstance(defaults),也就是说 axioscreateInstance()

于是执行createInstance(defaults) default是其他文件引入,用于参数初始化

因为default的代码很长,这里就不贴了,这里说明初始化都做了什么

lib/default.js

  1. 判断当前是浏览器环境还是node环境,分别使用xhrhttp
  2. 添加默认配置,例如header itemout
  3. 帮助我们默认添加xsrfCookieName,用于防止asrf攻击
  4. 等等很多初始化参数
完成初始化

初始化拦截器,添加原型链方法

当参数初始化完成后,被作为参数传递到createInstance()

  1. // defaultConfig 为初始化配置
  2. var context = new Axios(defaultConfig)
  3. 复制代码

执行了./lib/core/Axios.js

初始化拦截器

  1. // ...... 导入库
  2. /**
  3. * 创建Axios的新实例
  4. *
  5. * @param {Object} instanceConfig实例的默认配置
  6. */
  7. function Axios(instanceConfig) {
  8. // eslint-disable-next-line no-console
  9. this.defaults = instanceConfig // 将初始化的参数挂载到this上 便于原型的访问
  10. this.interceptors = {
  11. request: new InterceptorManager(),
  12. response: new InterceptorManager()
  13. }
  14. }
  15. // ...... 等等原型链代码
  16. 复制代码

这里,可以看到this.interceptors,是不是很眼熟?这是我们使用的拦截器对象

所以需要看看,初始化的时候,拦截器内部是什么

./lib/core/InterceptorManager

  1. 'use strict';
  2. var utils = require('./../utils');
  3. function InterceptorManager() {
  4. this.handlers = [];
  5. }
  6. InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  7. this.handlers.push({
  8. fulfilled: fulfilled,
  9. rejected: rejected
  10. });
  11. // 这里返回没问题 但是为什么这么写?
  12. return this.handlers.length - 1;
  13. };
  14. InterceptorManager.prototype.eject = function eject(id) {
  15. if (this.handlers[id]) {
  16. this.handlers[id] = null;
  17. }
  18. };
  19. // 迭代所有已注册的拦截器
  20. InterceptorManager.prototype.forEach = function forEach(fn) {
  21. // fn > function
  22. utils.forEach(this.handlersfunction forEachHandler(h) {
  23. if (h !== null) {
  24. // 将拦截器里面的函数交给其回调
  25. fn(h);
  26. }
  27. });
  28. };
  29. module.exports = InterceptorManager;
  30. 复制代码

似乎简单的出乎意料,仅仅是初始化了一个数组,提供了use(push数组) eject(清空数组) forEach三个方法

new Axios完成后(原型方法还没有挂载前)

拦截器初始化完成了

初始化原型方法

这里是axios实现的核心

./lib/core/Axios.js

  1. // ...... new Axios()....
  2. /**
  3. * 发送请求方法
  4. *
  5. * @param {Object} config The config specific for this request (merged with this.defaults)
  6. */
  7. // 当我们请求`axios.request(//...) 参数就会Axios.prototype.request接收
  8. Axios.prototype.request = function request(config) {
  9. /*eslint no-param-reassign:0*/
  10. // 作者为我们提供axios('url',{})以及axios(config)两种写法
  11. if (typeof config === 'string') {
  12. config = arguments[1] || {}
  13. config.url = arguments[0]
  14. } else {
  15. config = config || {}
  16. }
  17. // 合并 defaultconfig与自定义的config
  18. config = mergeConfig(this.defaults, config)
  19. config.method = config.method ? config.method.toLowerCase() : 'get' // 默认没有都是get请求
  20. // 创造一个请求序列数组 第一位是发送请求的方法,第二位是空
  21. var chain = [dispatchRequest, undefined]
  22. var promise = Promise.resolve(config) // 创建一个promise对象,并把参数传递进去
  23. //this.interceptors.request = InterceptorManager() 如果定义拦截器 那么 这里的InterceptorManager内部的handlers就会存在你写的拦截器代码
  24. // 执行InterceptorManager原型链上的forEach事件
  25. this.interceptors.request.forEach(function unshiftRequestInterceptors(
  26. interceptor
  27. ) {
  28. // interceptor 为 执行InterceptorManager原型链上的forEach事件返回的 拦截器函数
  29. //把请求拦截器数组依从加入头部
  30. chain.unshift(interceptor.fulfilled, interceptor.rejected)
  31. })
  32. this.interceptors.response.forEach(function pushResponseInterceptors(
  33. interceptor
  34. ) {
  35. // 同理
  36. // 将接收拦截器数组一次加入尾部
  37. chain.push(interceptor.fulfilled, interceptor.rejected)
  38. })
  39. while (chain.length) {
  40. // shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
  41. // 形成一个promise调用链条
  42. promise = promise.then(chain.shift(), chain.shift())
  43. }
  44. return promise
  45. }
  46. Axios.prototype.getUri = function getUri(config) {
  47. config = mergeConfig(this.defaults, config)
  48. return buildURL(config.url, config.params, config.paramsSerializer).replace(
  49. /^\?/
  50. ''
  51. )
  52. }
  53. // 为支持的请求方法提供别名
  54. utils.forEach(
  55. ['delete''get''head''options'],
  56. function forEachMethodNoData(method) {
  57. /*eslint func-names:0*/
  58. // 这里提供了语法通,在axios的原型链条上面 增加'delete', 'get', 'head', 'options'直接调用的方法 实际上还是request
  59. Axios.prototype[method] = function(url, config) {
  60. return this.request(
  61. utils.merge(config || {}, {
  62. method: method,
  63. url: url
  64. })
  65. )
  66. }
  67. }
  68. )
  69. // 与上方同理
  70. utils.forEach(['post''put''patch'], function forEachMethodWithData(method) {
  71. /*eslint func-names:0*/
  72. Axios.prototype[method] = function(url, data, config) {
  73. return this.request(
  74. utils.merge(config || {}, {
  75. method: method,
  76. url: url,
  77. data: data
  78. })
  79. )
  80. }
  81. })
  82. module.exports = Axios
  83. 复制代码

这里的代码,比较晦涩难懂,但是实现的非常巧妙,我用以下文图来表达

当我们执行以下代码

  1. let axios = require('./lib/axios')
  2. axios.interceptors.request.use(
  3. function(config) {
  4. console.log(config);
  5. return config
  6. },
  7. function(error) {
  8. return Promise.reject(error)
  9. }
  10. )
  11. axios.interceptors.response.use(
  12. function(res) {
  13. console.log(res);
  14. return res
  15. },
  16. function(error) {
  17. console.log(error);
  18. }
  19. )
  20. console.log('请求开始前的准备个工作');
  21. axios.get('......')
  22. .then(res => {
  23. console.log(res)
  24. })
  25. .catch(res => {
  26. console.log(res)
  27. })
  28. 复制代码

这里由请求序列数组实现的promise执行链条,帮助我们实现了拦截器等功能

借用知乎熵与单子的代码本大佬的图示(我觉得简单明了)

再次引用熵与单子的代码本【源码拾遗】axios —— 极简封装的艺术的一句话

  1. 通过巧妙的利用unshift、push、shift等数组队列、栈方法,实现了请求拦截、执行请求、响应拦截的流程设定,注意无论是请求拦截还是响应拦截,越先添加的拦截器总是越“贴近”执行请求本身。
  2. 复制代码

对原型链上的方法的处理

不知 道目前为止 看起来似乎一起变的比较明了,但是还存在一些问题

request get post 等等方法都在Axios.prototype上面,也就是说目前为止还无法直接调用原型链上的方法

于是我们看**./lib/axios.js**的 createInstance(defaultConfig)后面还没有看的代码

  1. function createInstance(defaultConfig) {
  2. console.log(defaultConfig);
  3. // defaultConfig 为初始化配置
  4. var context = new Axios(defaultConfig)
  5. // 将Axios.prototype.request的this传递给context
  6. // bind(Axios.prototype.request, context)的目的是让axios默认及request方法
  7. var instance = bind(Axios.prototype.request, context)
  8. // var instance = context
  9. // 将Axios.prototype的原型链上的对象复制到instance上面
  10. utils.extend(instance, Axios.prototype)
  11. // utils.extend(instance, Axios.prototype, context)
  12. // 将拦截器,默认配置复制到instance(这里主要是拦截器操作)
  13. utils.extend(instance, context)
  14. return instance
  15. }
  16. 复制代码

着重看这几行

  1. var instance = bind(Axios.prototype.request, context)
  2. utils.extend(instance, Axios.prototype, context)
  3. utils.extend(instance, context)
  4. return instance
  5. 复制代码

这里用到了bind,所以我们还要看看bind

  1. module.exports = function bind(fn, thisArg) {
  2. return function wrap() {
  3. // 获取到axios(//...)里面的参数
  4. var args = new Array(arguments.length)
  5. for (var i = 0; i < args.length; i++) {
  6. args[i] = arguments[i]
  7. }
  8. // 将fn的this传递给thisArg 例如将axios('www.baidu.com') 就会执行这个warp函数最后将参数交给fn也就是Axios.prototype.request
  9. return fn.apply(thisArg, args)
  10. }
  11. }
  12. 复制代码

明显看出 经过处理的instance是一个对象

当我们使用

  1. axios('// .... 请求连接')
  2. 复制代码

就是执行wrap(),将参数,全部apply到fn上,也就是Axios.prototype.request上,这样的this指向的调换,即默认

  1. axios('//....') === Axios.prototype.request('//....')
  2. 复制代码

后面的extend,帮助我们将其他Axios的原型属性get post 等等,移植到返回出去的instance上面

  1. axios.get('//....') === Axios.prototype.get('//....')
  2. 复制代码

否则我们无法通过axios.get() axios.post进行调用

  1. /**
  2. * 为对象a拓展对象b的属性
  3. *
  4. * @param {Object} a 需要拓展的属性
  5. * @param {Object} b 要从中复制属性的对象
  6. * @param {Object} thisArg 要绑定函数的对象
  7. * @return {Object} The 返回值
  8. */
  9. function extend(a, b, thisArg) {
  10. console.log(b);
  11. forEach(b, function assignValue(val, key) {
  12. if (thisArg && typeof val === 'function') {
  13. a[key] = bind(val, thisArg); // 一般情况下 和 else 效果 我不是很明白这里bind的必要性
  14. } else {
  15. a[key] = val; // 将b的属性移植到 a上
  16. }
  17. });
  18. return a;
  19. }
  20. 复制代码

最后返回出去的instance,就是我们日常使用的axios了 ?

请求流程

最后根据上面的总结,梳理一下axios的请求过程

总结

​ 最近不是很忙,所以抽出时间,查看了axois的源码,本文仅仅很粗略的理了axios的执行过程,以及内部的一些实现思路,实际上axios源码的内容非常丰富,也还有很多模块没有说,例如 adapter dispatchRequest取消请求,等等,还有很多 工具函数 值得我们学习

​ 可能有些开发者也有想去读源码的心情,但是很多库文件之间依赖关系复杂,仅仅去读 是很难读懂的,所以,我们需要把源码运行起来,在关键地方debugger以及添加log更加容易理清思路.可以去clone我的环境搭建es6调试环境,进行axios源码的阅读

​ 相关文章已经有很多大佬解读,本篇文章为个人总结,如有错误,或者歧义 欢迎提出 欢迎交流???