共计 11852 个字符,预计需要花费 30 分钟才能阅读完成。
面试官:你理解 axios 的原理吗?有看过它的源码吗?
一、axios 的根本应用
对于 axios
的根本应用,上篇文章曾经有所波及,这里再略微回顾一下:
发送申请
import axios from 'axios';
axios(config) // 间接传入配置
axios(url[, config]) // 传入 url 和配置
axios[method](url[, option]) // 间接调用申请形式办法,传入 url 和配置
axios[method](url[, data[, option]]) // 间接调用申请形式办法,传入 data、url 和配置
axios.request(option) // 调用 request 办法
const axiosInstance = axios.create(config)
// axiosInstance 也具备以上 axios 的能力
axios.all([axiosInstance1, axiosInstance2]).then(axios.spread(response1, response2))
// 调用 all 和传入 spread 回调
申请拦截器
axios.interceptors.request.use(function (config) {
// 这里写发送申请前解决的代码
return config;
}, function (error) {
// 这里写发送申请谬误相干的代码
return Promise.reject(error);
});
响应拦截器
axios.interceptors.response.use(function (response) {
// 这里写失去响应数据后处理的代码
return response;
}, function (error) {
// 这里写失去谬误响应解决的代码
return Promise.reject(error);
});
勾销申请
// 形式一
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('xxxx', {cancelToken: source.token})
// 勾销申请 (申请起因是可选的)
source.cancel('被动勾销申请');
// 形式二
const CancelToken = axios.CancelToken;
let cancel;
axios.get('xxxx', {cancelToken: new CancelToken(function executor(c) {cancel = c;})
});
cancel('被动勾销申请');
二、实现一个简易版 axios
构建一个 Axios
构造函数,外围代码为 request
办法:
class Axios {constructor() { }
request(config) {
return new Promise(resolve => {const {url = '', method ='get', data = {}} = config;
// 发送 ajax 申请
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onload = function() {console.log(xhr.responseText)
resolve(xhr.responseText);
}
xhr.send(data);
})
}
}
导出 axios
实例:
// 最终导出 axios 的办法,即实例的 request 办法
function CreateAxiosFn() {let axios = new Axios();
let req = axios.request.bind(axios);
return req;
}
// 失去最初的全局变量 axios
let axios = CreateAxiosFn();
上述就曾经可能实现 axios({})
这种形式的申请。上面是来实现下 axios.method()
这种模式的申请:
// 定义 get,post... 办法,挂在到 Axios 原型上
const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
methodsArr.forEach(met => {Axios.prototype[met] = function() {console.log('执行'+met+'办法');
// 解决单个办法
if (['get', 'delete', 'head', 'options'].includes(met)) {// 2 个参数 (url[, config])
return this.request({
method: met,
url: arguments[0],
...arguments[1] || {}})
} else {// 3 个参数 (url[,data[,config]])
return this.request({
method: met,
url: arguments[0],
data: arguments[1] || {},
...arguments[2] || {}})
}
}
})
将 Axios.prototype
上的办法搬运到 request
上。首先实现一个工具类,实现将 b
办法混入到 a
,并且批改 this
指向:
const utils = {extend(a,b, context) {for(let key in b) {if (b.hasOwnProperty(key)) {if (typeof b[key] === 'function') {a[key] = b[key].bind(context);
} else {a[key] = b[key]
}
}
}
}
}
批改导出的办法:
function CreateAxiosFn() {let axios = new Axios();
let req = axios.request.bind(axios);
// 减少代码
utils.extend(req, Axios.prototype, axios)
return req;
}
构建拦截器的构造函数:
class InterceptorsManage {constructor() {this.handlers = [];
}
use(fullfield, rejected) {
this.handlers.push({
fullfield,
rejected
})
}
}
实现 axios.interceptors.response.use
和 axios.interceptors.request.use
:
class Axios {constructor() {
// 新增代码
this.interceptors = {
request: new InterceptorsManage,
response: new InterceptorsManage
}
}
request(config) {...}
}
执行语句 axios.interceptors.response.use
和 axios.interceptors.request.use
的时候,实现获取 axios
实例上的 interceptors
对象,而后再获取 response
或 request
拦截器,再执行对应的拦截器的 use
办法。
把 Axios
上的办法和属性搬到 request
过来:
function CreateAxiosFn() {let axios = new Axios();
let req = axios.request.bind(axios);
// 混入办法,解决 axios 的 request 办法,使之领有 get,post... 办法
utils.extend(req, Axios.prototype, axios)
// 新增代码
utils.extend(req, axios)
return req;
}
当初 request
也有了 interceptors
对象,在发送申请的时候,会先获取 request
拦截器的 handlers
的办法来执行。
首先将执行 ajax
的申请封装成一个办法:
request(config) {this.sendAjax(config)
}
sendAjax(config){
return new Promise(resolve => {
const {url
= '', method ='get', data = {}} = config;
// 发送 ajax 申请
console.log(config);
const xhr = new XMLHttpRequest();
xhr.open(method, url, true);
xhr.onload = function() {console.log(xhr.responseText)
resolve(xhr.responseText);
};
xhr.send(data);
})
}
取得 handlers
中的回调:
request(config) {
// 拦截器和申请组装队列
let chain = [this.sendAjax.bind(this), undefined] // 成对呈现的,失败回调临时不解决
// 申请拦挡
this.interceptors.request.handlers.forEach(interceptor => {chain.unshift(interceptor.fullfield, interceptor.rejected)
})
// 响应拦挡
this.interceptors.response.handlers.forEach(interceptor => {chain.push(interceptor.fullfield, interceptor.rejected)
})
// 执行队列,每次执行一对,并给 promise 赋最新的值
let promise = Promise.resolve(config);
while(chain.length > 0) {promise = promise.then(chain.shift(), chain.shift())
}
return promise;
}
chains
大略是 ['fulfilled1','reject1','fulfilled2','reject2','this.sendAjax','undefined','fulfilled2','reject2','fulfilled1','reject1']
这种模式。这样就可能胜利实现一个简易版 axios
。
三、源码剖析
首先看看目录构造:
axios
发送申请有很多实现的办法,实现入口文件为 axios.js
:
function createInstance(defaultConfig) {var context = new Axios(defaultConfig);
// instance 指向了 request 办法,且上下文指向 context,所以能够间接以 instance(option) 形式调用
// Axios.prototype.request 内对第一个参数的数据类型判断,使咱们可能以 instance(url, option) 形式调用
var instance = bind(Axios.prototype.request, context);
// 把 Axios.prototype 上的办法扩大到 instance 对象上,// 并指定上下文为 context,这样执行 Axios 原型链上的办法时,this 会指向 context
utils.extend(instance, Axios.prototype, context);
// Copy context to instance
// 把 context 对象上的本身属性和办法扩大到 instance 上
// 注:因为 extend 外部应用的 forEach 办法对对象做 for in 遍历时,只遍历对象自身的属性,而不会遍历原型链上的属性
// 这样,instance 就有了 defaults、interceptors 属性。utils.extend(instance, context);
return instance;
}
// Create the default instance to be exported 创立一个由默认配置生成的 axios 实例
var axios = createInstance(defaults);
// Factory for creating new instances 扩大 axios.create 工厂函数,外部也是 createInstance
axios.create = function create(instanceConfig) {return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// Expose all/spread
axios.all = function all(promises) {return Promise.all(promises);
};
axios.spread = function spread(callback) {return function wrap(arr) {return callback.apply(null, arr);
};
};
module.exports = axios;
次要外围是 Axios.prototype.request
,各种申请形式的调用实现都是在 request
外部实现的,简略看下 request
的逻辑:
Axios.prototype.request = function request(config) {// Allow for axios('example/url'[, config]) a la fetch API
// 判断 config 参数是否是 字符串,如果是则认为第一个参数是 URL,第二个参数是真正的 config
if (typeof config === 'string') {config = arguments[1] || {};
// 把 url 搁置到 config 对象中,便于之后的 mergeConfig
config.url = arguments[0];
} else {
// 如果 config 参数是否是 字符串,则整体都当做 config
config = config || {};}
// 合并默认配置和传入的配置
config = mergeConfig(this.defaults, config);
// 设置申请办法
config.method = config.method ? config.method.toLowerCase() : 'get';
/*
something... 此局部会在后续拦截器独自讲述
*/
};
// 在 Axios 原型上挂载 'delete', 'get', 'head', 'options' 且不传参的申请办法,实现外部也是 request
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {Axios.prototype[method] = function(url, config) {return this.request(utils.merge(config || {}, {
method: method,
url: url
}));
};
});
// 在 Axios 原型上挂载 'post', 'put', 'patch' 且传参的申请办法,实现外部同样也是 request
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {Axios.prototype[method] = function(url, data, config) {return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});
request
入口参数为 config
,能够说 config
贯彻了 axios
的毕生。axios
中的 config
次要散布在这几个中央:
- 默认配置
defaults.js
config.method
默认为get
- 调用
createInstance
办法创立axios
实例,传入的config
- 间接或间接调用
request
办法,传入的config
// axios.js
// 创立一个由默认配置生成的 axios 实例
var axios = createInstance(defaults);
// 扩大 axios.create 工厂函数,外部也是 createInstance
axios.create = function create(instanceConfig) {return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// Axios.js
// 合并
默认配置和传入的配置
config = mergeConfig(this.defaults, config);
// 设置申请办法
config.method = config.method ? config.method.toLowerCase() : 'get';
从源码中,能够看到优先级:默认配置对象 default
< method:get
< Axios
的实例属性 this.default
< request
参数。
上面重点看看 request
办法:
Axios.prototype.request = function request(config) {
/*
先是 mergeConfig ... 等,不再论述
*/
// Hook up interceptors middleware 创立拦截器链。dispatchRequest 是重中之重,后续重点
var chain = [dispatchRequest, undefined];
// push 各个拦截器办法 留神:interceptor.fulfilled 或 interceptor.rejected 是可能为 undefined
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// 申请拦截器逆序 留神此处的 forEach 是自定义的拦截器的 forEach 办法
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 响应拦截器程序 留神此处的 forEach 是自定义的拦截器的 forEach 办法
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 初始化一个 promise 对象,状态为 resolved,接管到的参数为曾经解决合并过的 config 对象
var promise = Promise.resolve(config);
// 循环拦截器的链
while (chain.length) {promise = promise.then(chain.shift(), chain.shift()); // 每一次向外弹出拦截器
}
// 返回 promise
return promise;
};
拦截器 interceptors
是在构建 axios
实例化的属性:
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {request: new InterceptorManager(), // 申请拦挡
response: new InterceptorManager() // 响应拦挡};
}
InterceptorManager
构造函数:
// 拦截器的初始化 其实就是一组钩子函数
function InterceptorManager() {this.handlers = [];
}
// 调用拦截器实例的 use 时就是往钩子函数中 push 办法
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
// 拦截器是能够勾销的,依据 use 的时候返回的 ID,把某一个拦截器办法置为 null
// 不能用 splice 或者 slice 的起因是 删除之后 id 就会变动,导致之后的程序或者是操作不可控
InterceptorManager.prototype.eject = function eject(id) {if (this.handlers[id]) {this.handlers[id] = null;
}
};
// 这就是在 Axios 的 request 办法中 中循环拦截器的办法 forEach 循环执行钩子函数
InterceptorManager.prototype.forEach = function forEach(fn) {utils.forEach(this.handlers, function forEachHandler(h) {if (h !== null) {fn(h);
}
});
}
申请拦截器办法是被 unshift
到拦截器中,响应拦截器是被 push
到拦截器中的。最终它们会拼接上一个叫 dispatchRequest
的办法被后续的 promise
程序执行:
var utils = require('./../utils');
var transformData = require('./transformData');
var isCancel = require('../cancel/isCancel');
var defaults = require('../defaults');
var isAbsoluteURL = require('./../helpers/isAbsoluteURL');
var combineURLs = require('./../helpers/combineURLs');
// 判断申请是否已被勾销,如果曾经被勾销,抛出已勾销
function throwIfCancellationRequested(config) {if (config.cancelToken) {config.cancelToken.throwIfRequested();
}
}
module.exports = function dispatchRequest(config) {throwIfCancellationRequested(config);
// 如果蕴含 baseUrl,并且不是 config.url 绝对路径,组合 baseUrl 以及 config.url
if (config.baseURL && !isAbsoluteURL(config.url)) {
// 组合 baseURL 与 url 造成残缺的申请门路
config.url = combineURLs(config.baseURL, config.url);
}
config.headers = config.headers || {};
// 应用 /lib/defaults.js 中的 transformRequest 办法,对 config.headers 和 config.data 进行格式化
// 比方将 headers 中的 Accept,Content-Type 对立解决成大写
// 比方如果申请注释是一个 Object 会格式化为 JSON 字符串,并增加 application/json;charset=utf-8 的 Content-Type
// 等一系列操作
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// 合并不同配置的 headers,config.headers 的配置优先级更高
config.headers = utils.merge(config.headers.common || {},
config.headers[config.method] || {},
config.headers || {});
// 删除 headers 中的 method 属性
utils.forEach(['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {delete config.headers[method];
}
);
// 如果 config 配置了 adapter,应用 config 中配置 adapter 的代替默认的申请办法
var adapter = config.adapter || defaults.adapter;
// 应用 adapter 办法发动申请(adapter 依据浏览器环境或者 Node 环境会有不同)return adapter(config).then(
// 申请正确返回的回调
function onAdapterResolution(response) {
// 判断是否曾经勾销了申请,如果勾销了申请抛出已勾销
throwIfCancellationRequested(config);
// 应用 /lib/defaults.js 中的 transformResponse 办法,对服务器返回的数据进行格式化
// 例如,应用 JSON.parse 对响应注释进行解析
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);
}
);
};
- 在
dispatchRequest
中首先判断是否曾经勾销了申请,如果已勾销,抛出已勾销 - 合并不同配置的 headers,config.headers 的配置优先级更高
- 应用
config.adapter
发动申请(adapter 依据浏览器环境或者 Node 环境会有不同)
源码中有一个比拟乏味的点是 transformData
这个办法:
var utils = require('./../utils');
var defaults = require('../defaults');
module.exports = function transformData(data, headers, fns) {
/*eslint no-param-reassign:0*/
utils.forEach(fns, function transform(fn) {data = fn(data, headers);
});
return data;
};
transformData
其实就是执行 config.transformRequest
和 config.transformResponse
:
// 应用 /lib/defaults.js 中的 transformRequest 办法,对 config.headers 和 config.data 进行格式化
// 比方将 headers 中的 Accept,Content-Type 对立解决成大写
// 比方如果申请注释是一个 Object 会格式化为 JSON 字符串,并增加 application/json;charset=utf-8 的 Content-Type
// 等一系列操作
config.data = transformData(
config.data,
config.headers,
config.transformRequest
);
// 应用 /lib/defaults.js 中的 transformResponse 办法,对服务器返回的数据进行格式化
// 例如,应用 JSON.parse 对响应注释进行解析
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);
四、总结
axios
是一个基于 Promise 的 HTTP 申请客户端,具备弱小的拦截器性能,反对申请和响应的拦挡解决,同时提供了丰盛的配置选项。在理论开发中,能够利用 axios
来发动 HTTP 申请,解决响应数据,实现前后端的数据交互。通过深刻了解 axios
的原理和源码,能够更好地应用和扩大它,满足简单的业务需要。
参考文献
- https://juejin.cn/post/6856706569263677447#heading-4
- https://juejin.cn/post/6844903907500490766
- https://github.com/axios/axios