axios源码梳理 - 了解请求库是怎么实现的

axios这个请求库很多人都用过, 那么有下面几个问题, 你能回答上来吗?

  1. 为什么 axios 既可以当函数调用,也可以当对象使用,比如axios({})、axios.get。
  2. 简述 axios 调用流程。
  3. 有用过拦截器吗?原理是怎样的?
  4. 有使用axios的取消功能吗?是怎么实现的?
  5. 为什么支持浏览器中发送请求也支持node发送请求?

上个月看的axios源码, 看完之后对axios的原理有了大概的理解, 今天就把自己的理解重新梳理一下.

axios源码目录

我太懒了, 懒的自己整理目录, 参考掘金文章 - 逐行解析Axios源码

# 📁 lib
# |——  📁 adapters // axios主要使用的请求方法
# |——  |——  📃 http.js  // axios中node端使用的请求函数
# |——  |——  📃 xhr.js  // axios中浏览器端使用的请求函数
# |——  📁 cancel
# |——  |——  📃 Cancel.js // 定义了,取消请求返回的信息结构
# |——  |——  📃 CancelToken.js // 定义了用于取消请求的主要方法
# |——  |——  📃 isCancel.js // 判断是否是取消请求的信息
# |——  📁 core
# |——  |——  📃 Axios.js // Axios类
# |——  |——  📃 dispatchRequest.js // 发起请求的地方 
# |——  |——  📃 InterceptorManager.js // InterceptorManager类,拦截器类
# |——  |——  📃 mergeConfig.js // 合并配置项
# |——  |——  📃 settle.js // 根据请求状态,处理Promise
# |——  |——  📃 createError.js // 生成指定的error
# |——  |——  📃 enhanceError.js // 指定error对象的toJSON方法
# |——  |——  📃 transformData.js // 使用default.js中transformRequest和transformResponse对响应以及请求进行格式化
# |——  📁 helpers
# |——  |——  📃 bind.js // 工具函数
# |——  |——  📃 parseHeaders.js // 将getAllResponseHeaders返回的header信息转化为对象
# |——  |——  📃 buildURL.js // 将params参数
# |——  |——  📃 cookies.js // 封装了读取,写入,删除cookies的方法
# |——  |——  📃 isURLSameOrigin.js // 检测当前的url与请求的url是否同源
# |——  |——  📃 normalizeHeaderName.js // 对对象属性名的进行格式化,删除,新建符合大小写规范的属性
# |——  |——  📃 combineURLs.js // 组合baseurl
# |——  |——  📃 isAbsoluteURL.js // 判断是否为绝对路径(指的://或//开头的为绝对路径)
# |——  📃 axios.js
# |——  📃 defaults.js // axios中默认配置
# |——  📃 utils.js // 一些工具方法
# |——  |——  ⏹ isFormData // 判断是否是formData对象
# |——  |——  ⏹ isStandardBrowserEnv // 判断当前环境是否为标准浏览器环境
# |——  |——  ⏹ isUndefined // 判断是否为undefined
# |——  |——  ⏹ merge
# |——  |——  ⏹ isURLSearchParams // 判断是否为URLSearchParams对象

具体请求流程

入口文件axios.js

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;
}
var axios = createInstance(defaults);

// Expose Axios class to allow class inheritance
// 暴露 Axios calss 允许 class 继承
axios.Axios = Axios;
...
//axios上挂载其他属性
...

//导出axios
module.exports = axios

我们每次使用的axios都是这里导出的axios, 它是由构造函数createInstance构造的实例, 并且在实例上添加了一些其他的属性, 比如Axios

createInstance构造函数

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

1. 通过Axios生成一个实例对象context

2. 将Axios.prototype.request这个方法绑定到context

var instance = bind(Axios.prototype.request, context);
//等同于var instance = Axios.prototype.request.bind(context)

所以此处的instance就是绑定了this到context上的Axios.prototype.request函数

3. 将Axios.prototype上的方法继承到instance上

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

//代码功能类似于下面这段, 但是下面这段没有绑定this的功能, 上面的extend方法有绑定this的功能
Object.keys(Axios.prototype).forEach((method) => {
        instance[method] = Axios.prototype[method]
    })

4. 最后再把context的功能继承到instance上,最后返回instance

utils.extend(instance, context)
return instance

Axios.prototype.request

Axios.prototype.request = function request(config) {
    //...省略
    //核心代码如下
  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;
};

核心代码就是上面这一块, 主要是处理拦截器的功能. (这点后面详细讲)

  • 主要就是维护了一个请求chain
  • dispatchRequest 发送请求, 请求拦截器添加在promise chain前, 响应拦截器添加在promise chain后面.
  • [dispatchRequest, undefined]中的undefined主要是作为promise reject的一个占位

其他请求方法

源码中Axios中下面这块代码就是将其他方法添加到Axios.prototype上, 上文提到在createInstace中将其他方法绑定到instance上, 所以我们调用get等方法时是走到下面这块代码的

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

当调用Axios.prototype[method]时, 其实本质上还是调用的this.request, 所以最终都会走到上一小节的Axios.prototype.request

总结: Axios的流程

createInstance方法本质上返回的就是Axios.prototype.request.bind(context), 所以当我们使用axios({})这种方法调用时,直接走的就是Axios.prototype.request;

当我们使用axios.get这种方法调用时, 是因为在最后返回的时候将Axios.prototype上的其他方法挂载到了instance上, 所以我们走的是Axios.prototype.get上, 但是Axios.prototype.get本质上还是返回了Axios.prototype.request

所以最终都是通过Axios.prototype.request 这个方法统一处理请求

拦截器原理

拦截器使用

我们先来看一下拦截器是怎么使用的

axios.interceptors.request.use(
	function (config) {
		config.headers.traceId = getTraceId()
		return config
	},
	function (error) {
		// 对请求错误做些什么
		return Promise.reject(error)
	}
)
axios.interceptors.response.use(
	function (response) {
		return response.data
	},
	function (error) {
		if (error.response && error.response.status == 401) {
		//某些处理
		}
		return error.response.data
	}
)

由代码可知, 是在axios.interceptorsrequest和response的use函数中添加了处理函数

下面我们看下具体实现

拦截器实现

构造函数Axios中存储了interceptors变量, interceptors中分别有request和response

function Axios(name) {
    this.name = name
    this.interceptors = {
        request: new InterceptorManager(),
        response: new InterceptorManager(),
    }
}
//InterceptorManager
function InterceptorManager() {
    this.handlers = []
}

InterceptorManager.prototype.use = function use(fulfilled, rejected) {
    //将fulfilled和rejected添加到handlers中
    this.handlers.push({
        fulfilled: fulfilled,
        rejected: rejected,
    })
    return this.handlers.length - 1
}

所以就在Axios中可以通过this.interceptors.request 拿到拦截器中的处理函数

    //Axios.prototype.request中
    this.interceptors.request.forEach((interceptor) => {
        chain.unshift(interceptor.fulfilled, interceptor.rejected)
    })
    this.interceptors.response.forEach((interceptor) => {
        chain.push(interceptor.fulfilled, interceptor.rejected)
    })
    // 依次执行`promise chain`的处理函数
    while (chain.length) {
        promise = promise.then(chain.shift(), chain.shift())
    }

此处有个问题,不知道有人发现没. 就是 InterceptorManager中将处理函数保存在this.handlers中, 为什么在this.interceptors.response可以直接forEach拿到呢?

原因是InterceptorManager原型上重写了forEach方法, 在这里遍历的this.handles

InterceptorManager.prototype.forEach = function forEach(fn) {
    utils.forEach(this.handlers, function forEachHandler(h) {
        if (h !== null) {
            fn(h)
        }
    })
}

总结

所以, axios的拦截器是在拦截器构造器中定义了use方法来定义拦截处理函数, 并重写了forEach方法来拿到拦截处理函数

Axios.prototype.request 中, 将请求拦截和响应拦截分别添加到promise chain数组的前面和后面. 这样就实现了拦截

取消请求原理

取消请求的实现

想要了解原理, 首先还是要来看一下取消请求的时候要做什么怎么做

//第一种方法
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 {
    // 处理错误
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
//第二种方法
const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// 取消请求
cancel();

取消请求实现

第一步

在axios实例中添加CancelToken

let axios = createInstance()
axios.CancelToken = require('./cancel/CancelToken')
module.exports = axios

第二步

看下CancelToken实现

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

    var resolvePromise
    //取消函数被调用时, this.promise其实被调用了resolve, 用于在发送请求时处理
    this.promise = new Promise(function promiseExecutor(resolve) {
        resolvePromise = resolve
    })

    var token = this
    //cancel是传递给外面source函数的取消函数
    executor(function cancel(message) {
        console.log('执行取消请求了, message:', message)
        if (token.reason) {
            // Cancellation has already been requested
            return
        }
        //给token加一个reason属性
        token.reason = new Cancel(message)
        resolvePromise(token.reason)
    })
}

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

所以不管是第一种方法CancelToken.source().cancel() 还是第二种方法

new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })

都是调用的executor方法

值得注意的是, 在调用exector时, 相当于调用了this.promise的resolve

var resolvePromise
    //取消函数被调用时, this.promise其实被调用了resolve, 用于在发送请求时处理
    this.promise = new Promise(function promiseExecutor(resolve) {
        resolvePromise = resolve
    })

//在发送请求中, 根据this.promise做处理
// xhr
if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }
    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

其实也就相当于下面这样

var resolvePromise
//取消函数被调用时, this.promise其实被调用了resolve, 用于在发送请求时处理
this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve
})
resolvePromise(1)

this.promise.then((res) => {
    console.log(res) //1
})

总结

axios的取消请求模块, 主要就是在axios上挂载了CancelToken, 在CancelToken中的source函数中返回了两个数据token和取消方法cancel

其中cancel被调用时就调用了executor方法, 这个方法会调用一个this.promise的resolve

那么在请求模块处理Adapter中, 就会处理这次请求取消

浏览器和node发送请求

为什么可以在浏览器和node中发送请求

看下源码

//dispatchRequest.js中
var defaults = require('../defaults');
//defaults.js中
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;
}

在这里做了一层判断, 分别适配了浏览器的XMLHttpRequestnode环境

node环境中用的就是node封装的http和https模块

//http.js
var http = require('http');
var https = require('https');

总结

好了, 这就是axios的重要功能源码了

第一次读源码, 算是把整体结构弄清楚了. 其实也不难, 不要自己吓自己

一个月之前看的了, 今天算是做个总结吧, 免得以后忘记了

最后放一张自己看源码时画的图吧

image

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值