axios这个请求库很多人都用过, 那么有下面几个问题, 你能回答上来吗?
- 为什么 axios 既可以当函数调用,也可以当对象使用,比如axios({})、axios.get。
- 简述 axios 调用流程。
- 有用过拦截器吗?原理是怎样的?
- 有使用axios的取消功能吗?是怎么实现的?
- 为什么支持浏览器中发送请求也支持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.interceptors
的request和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;
}
在这里做了一层判断, 分别适配了浏览器的XMLHttpRequest
和node
环境
node环境中用的就是node封装的http和https模块
//http.js
var http = require('http');
var https = require('https');
总结
好了, 这就是axios的重要功能源码了
第一次读源码, 算是把整体结构弄清楚了. 其实也不难, 不要自己吓自己
一个月之前看的了, 今天算是做个总结吧, 免得以后忘记了
最后放一张自己看源码时画的图吧