接着上篇 如何让axios在vue中丝滑起来。对axios的内部原理甚是好奇,近两天看了他的源码,分析了一些,确实值得80K的star。
另外,本文仅是浅浅的分析实现。至于有些地方为什么那样实现,代码设计的精妙,以及涉及到的一些设计模式,限于能力,遇到只能说句
“卧槽!还可以这样写,绝啊!“
⚾准备
随便找个测试项目 ,本次分析版本的是 @v0.21.0
cnpm install axios@0.21.0
然后从node_modules里面找到 axios
一步步解开神秘的面纱。
既然看源码了,那么对axios的基本API⇲应该是很熟悉了。不然会增加理解成本!
本文配和源码享用,可能更香哦!
⚾源码结构
从 pakage.json
文件知道,index.js
是入口文件。
另外lib
是源代码文件, dist
是打包文件。
下面是文件目录(部分文件已被删除)
📦axios
┣ 📂dist
┣ 📂lib
┃ ┣ 📂adapters //请求方法
┃ ┃ ┣ 📜http.js // node环境请求
┃ ┃ ┣ 📜README.md
┃ ┃ ┗ 📜xhr.js // 浏览器环境请求 xmlRequest
┃ ┣ 📂cancel //取消请求相关
┃ ┃ ┣ 📜Cancel.js
┃ ┃ ┣ 📜CancelToken.js
┃ ┃ ┗ 📜isCancel.js
┃ ┣ 📂core
┃ ┃ ┣ 📜Axios.js // class Axios
┃ ┃ ┣ 📜buildFullPath.js
┃ ┃ ┣ 📜createError.js // 统一的错误创建
┃ ┃ ┣ 📜dispatchRequest.js // 执行请求
┃ ┃ ┣ 📜enhanceError.js // error.toJSon
┃ ┃ ┣ 📜InterceptorManager.js //拦截器定义
┃ ┃ ┣ 📜mergeConfig.js // 配置合并函数
┃ ┃ ┣ 📜README.md
┃ ┃ ┣ 📜settle.js //
┃ ┃ ┗ 📜transformData.js // 数据转换
┃ ┣ 📂helpers //辅助工具函数
┃ ┃ ┣ 📜bind.js
┃ ┃ ┣ 📜buildURL.js
┃ ┃ ┣ 📜combineURLs.js
┃ ┃ ┣ 📜cookies.js
┃ ┃ ┣ 📜deprecatedMethod.js
┃ ┃ ┣ 📜isAbsoluteURL.js
┃ ┃ ┣ 📜isAxiosError.js
┃ ┃ ┣ 📜isURLSameOrigin.js
┃ ┃ ┣ 📜normalizeHeaderName.js //规范headers属性名
┃ ┃ ┣ 📜parseHeaders.js
┃ ┃ ┣ 📜README.md
┃ ┃ ┗ 📜spread.js
┃ ┣ 📜axios.js // 对外暴露接口
┃ ┣ 📜defaults.js
┃ ┗ 📜utils.js
┣ 📂node_modules
┣ 📜index.js //入口文件
┣ 📜package.json
可能有的朋友就要问了,靓仔,你这目录打印的怪好看呢,我怎么才能像你一样打印出如此美观的目录(windows下)。
为了满足这部分朋友,介绍两个 vsCode 插件🐶
file-tree-generator
(推荐)ascii Tree Genertaor
(简约)
⚾如何实现请求的一条龙服务
初始化
首先,当
important axios from 'axios';
上面引用 index.js
, index.js
引用 axios.js
🧾axios.js
前面时一个创建实例的方法,具体每一步做什么代码中已注释
function createInstance(defaultConfig) {
// 创建一个axios的实例
var context = new Axios(defaultConfig);
// 将axios 的请求方法绑定为上面实例作为上下文,和bind一样
var instance = bind(Axios.prototype.request, context);
// 将原axios.prototype上方法复制到axios,并绑定上下文为context
utils.extend(instance, Axios.prototype, context);
// 将context的方法复制给instance
utils.extend(instance, context);
return instance;
}
上面方法什么时候用呢,在返回 axios, 或者调用 axios.create 的时候被调用!
// 创建一个默认导出的实例
var axios = createInstance(defaults);
// 暴露axios类以允许类的继承
axios.Axios = Axios;
// 用于创建新实例的工厂
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// 暴露Cancel与token, 取消请求后面介绍
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
// axios 的all方法就是调用Promise.all
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread = require('./helpers/spread');
// Expose isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');
axios.js 做了什么事?
- 定义了创建实例的方法
- 补齐axios的属性/方法,并暴露出去
上面createInstance
里面,主要是Axios
有很大的作用, 让我们来康康。
🧾Core/Axios.js
function Axios(instanceConfig) {
this.defaults = instanceConfig;
// 定义了拦截器属性
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
哦,原来 Axios 是个函数!需要传入一些配置作为它的 defaults
属性,这里的配置就是下面使用中传入的 config
axios(config)
axios.get(url, config);
axios.post(url, data, config);
var instance = axios.create(config);
接着来看,
Axios.prototype.request = function request(config) {
// 这里的config是请求拦截器里面来的,下面做一些格式的处理
if (typeof config === 'string') { // 是字符串就当做url处理
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}
// 合并配置
config = mergeConfig(this.defaults, config);
// Set config.method
if (config.method) {
config.method =
config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}
// 连接拦截器中间件,这里的dispatchRequest就是分发请求的方法,参考后面[分发请求]
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);// 这里传入初始的配置,可以供请求拦截器修改配置。
//调用拦截器自定义方法forEach,参考后面[拦截器]
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// interceptor 就是我们传入拦截器的两个函数, 然后插入chain数组开头
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 向chain数组末尾添加元素
chain.push(interceptor.fulfilled, interceptor.rejected);
});
//while循环一步步利用then链向下执行
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
/*
相当于
promise.then(request_fullfilled, request_rejected)
.then(dispatchRequestFunc, undefined)
.then(response_fullfilled, response_rejected)
*/
}
return promise;
};
哦,原来 axios.prototype
还有个 request
方法,后面 chain 链
设计的很棒,我必须说一句
“卧槽!还可以这样写,绝啊!“
- 满足了可添加多个拦截器
- 满足了每一步都异步执行
- 优雅优雅优雅!!
注意 | 每次从数组里面取的都是前两个(shift会改变数组)执行,这回造成当有多个拦截器的时候,请求拦截器先加入后执行,相应拦截器先加入先执行
补充一下拦截器的使用,为了方便理解,匿名函数我给了名字。
// 添加请求拦截器
axios.interceptors.request.use(
function request_fullfilled(config) {return config},
function request_rejected(error) { return Promise.reject(error)}
);
// 添加响应拦截器
axios.interceptors.response.use(
function response_fullfilled(response) { return response},
function response_rejected(error) { return Promise.reject(error)}
);
大家好好想想,文字描述不如看代码直接。
我当时看到这个设计,嘴里不断发出 啧~ 啧~ 啧~ 的赞叹!😱
继续看下面🐶
//原型上定义一个将请求参数格式化的方法。类似{a:1, b:2} 变成 a=1&b=2 的功能
Axios.prototype.getUri = function getUri(config) {
config = mergeConfig(this.defaults, config);
return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};
// 将所有方法添加到Axios原型上,且相应的调用并返回注入配置的请求方法的返回值。
// 并将data添加到config.data上面,
// 为支持的请求方法提供别名
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: (config || {}).data
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(mergeConfig(config || {}, {
method: method,
url: url,
data: data
}));
};
});
注意最后 将 delete
、get
、post
、… 方法添加到了原型上。这个文件相对比较重要,我认为有承前启后,贯穿上下的作用,所以做了详细介绍。那 Axios.js
做了什么事呢?
- .deaults 属性保存了配置项
- 配置了请求/响应拦截器
- 在
Axios.prototype
上定义了一些它方法(request,getUri
,get
,post
…)
上面 forEach
方法是干的什么的呢,就像 Array.prototype.forEach
一样
Core/untils
/*
为obj的每一项执行一次fn。
如果'obj'是数组,则调用回调传递每个项的值、索引和完整数组。
如果“obj”是对象,则调用回调传递每个属性的值、键和完整对象。
*/
function forEach(obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return;
}
if (typeof obj !== 'object') {
obj = [obj];
}
//数组和对象分开处理
if (isArray(obj)) {
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
每次调用 axios.get
= > Axios.prototype.get
=> Axios.protype.request
注意Axios实例上的方法和原型上的方法都在axios.js createInstance 复制给了axios
所以 axios[method] 的语法糖也就好解释了,每次 Method 都是触发 request 方法来请求的。
request 先放放,下面来看看拦截器里面是什么东西
拦截器
在🧾Axios.js,有这样的代码
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
下面进入 InterceptorManager
🧾Core/InterceptorManager.js
// 定义一个拦截器方法,并且返回所在handers的位置下标作为清除的id
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};
// 移除拦截器的方法,将id置空
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};
// 定义一个forEach方法,对所有的拦截器就行迭代
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) { //确保拦截器未被移除
fn(h); // h = {fulfilled: fulfilled, rejected: rejected}
}
});
};
代码不多,结合注释理解即可。
注意 | 这里的自定义 forEach方法
在 axios.js
拦截器里使用过
接着我们来看request如何请求
分发请求
request
为啥能发出请求,主要是 dispatchRqeust
。听说它很勇,走,一起来康康🤭。
🧾Core/dispactchRequest.js
// 如果配置了取消,则抛出取消信息,取消请求参考[带着问题加深理解]
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}
// 使用配置的适配器向服务器发送请求
module.exports = function dispatchRequest(config) {
throwIfCancellationRequested(config);
config.headers = config.headers || {};
/*
function transformData(data, headers, fns) {
utils.forEach(fns, function transform(fn) {
data = fn(data, headers);
});
return data;
};
*/
// 将config.headers的一些字段规范化,不规范的设置为无效取默认值。
// 将config.data格式化,上面是transformData函数
config.data = transformData(
config.data,
config.headers,
config.transformRequest // 如果没写,则来自 defaults 的默认配置,参见[默认配置]
);
// 打平配置头部, .common 参见[默认配置]
config.headers = utils.merge(
config.headers.common || {},
config.headers[config.method] || {},
config.headers
);
// 清除headers上的方法, 因为这些并不是headers的合法属性, 如果需后台允许headers自定义属性
utils.forEach(
['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
function cleanHeaderConfig(method) {
delete config.headers[method];
}
);
//定义是适配器,可自定义
var adapter = config.adapter || defaults.adapter;
// adapter 返回的是个promise
return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);
// 转换响应数据
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);
});
};
上面文件做啥了?
config
配置的规范转换,与默认配置合并- 利用适配器
Adapter
发出请求,并将响应数据做转换。
注意|dispatchRequest
的结果会返回到响应拦截器(如果有)或者axios(config).then
的回调
上面文件有两个关键的东西,一是 默认配置,二是 适配器,我们先看 默认配置
默认配置
默认配置主要在defaults.js里
🧾Core/defaults.js
var DEFAULT_CONTENT_TYPE = {
'Content-Type': 'application/x-www-form-urlencoded'
};
// 设置默认的content-Type
function setContentTypeIfUnset(headers, value) {
if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
headers['Content-Type'] = value;
}
}
// 获取默认的适配器,不同环境调用不用
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') { // 对于浏览器,使用XHR适配器
adapter = require('./adapters/xhr');
} else if (
typeof process !== 'undefined' &&
Object.prototype.toString.call(process) === '[object process]') { //对于node使用HTTP适配器
adapter = require('./adapters/http');
}
return adapter;
}
var defaults = {
adapter: getDefaultAdapter(),
// 默认转换请求数据函数,这里就是一些data,headers的转换
transformRequest: [function transformRequest(data, headers) {
// 规范头部字段名,如果写的不对,则取默认值
normalizeHeaderName(headers, 'Accept');
normalizeHeaderName(headers, 'Content-Type');
if (utils.isFormData(data) ||
utils.isArrayBuffer(data) ||
utils.isBuffer(data) ||
utils.isStream(data) ||
utils.isFile(data) ||
utils.isBlob(data)
) {
return data;
}
if (utils.isArrayBufferView(data)) {
return data.buffer;
}
//下面两个 if 重置headers的contentType
//[了解更多⇲](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type)
if (utils.isURLSearchParams(data)) {
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString();
}
if (utils.isObject(data)) {
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
return data;
}],
//默认转换响应数据函数
transformResponse: [function transformResponse(data) {
/*eslint no-param-reassign:0*/
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}],
//超时时间,如果请求超过此时间(ms),会被取消,如果为0,则会一直请求。
timeout: 0,
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
maxContentLength: -1,
maxBodyLength: -1,
// 关于状态码的解释: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
// 默认是把status为 2xx 请求数据传入成功回调
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};
defaults.headers = {
common: {
'Accept': 'application/json, text/plain, */*'
}
};
// 将get、delete...方法添加到 heades上面
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
defaults.headers[method] = {};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
这文件干了什么?
- 定义了一些初始值和配置数据转换的默认方法
- 定义了根据环境获取适配器的方法
接着我们康康🤓适配器
适配器
适配器就是最接近发出请求的地方,这里不分析node环境的http方式,稍微分析一些xhr的方式.
先来个基本的xhr的请求。来自MDN⇲
var xhr= new XMLHttpRequest(),
method = "GET",
url = "https://developer.mozilla.org/";
xhr.open(method, url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(xhr.responseText)
}
}
xhr.send();
为节省篇幅,不多做介绍,这个知识点很大,建议 MDN⇲ 上去了解
🧾adapters/xhr.js
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
if (utils.isFormData(requestData)) {
delete requestHeaders['Content-Type']; // Let the browser set it
}
//关于XMLHttpRequest https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
var request = new XMLHttpRequest();
// http身份认证,有些请求你可以定制某些身份才可以访问
if (config.auth) {
var username = config.auth.username || '';
var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}
// 将基础baseUrl与请求url进行组合
var fullPath = buildFullPath(config.baseURL, config.url);
// 初始化一个请求
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
request.timeout = config.timeout;
// XMLHttpRequest 的readyState 属性发生改变时触发 readystatechange (en-US) 事件的时候被调用
request.onreadystatechange = function handleLoad() {
// 关于readyState的状态,https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/readyState
if (!request || request.readyState !== 4) {
return;
}
if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
return;
}
// 将头部字段解析为对象 getAllResponseHeaders
//参考(https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/getAllResponseHeaders)
var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
//responseText 是一个纯文本的值, response 是由responseType决定,可能有多个值;
// [参考]x(https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/response)
var response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config: config,
request: request
};
/*
function settle(resolve, reject, response) {
var validateStatus = response.config.validateStatus;
if (!response.status || !validateStatus || validateStatus(response.status)) {
resolve(response);
} else {
reject(createError(
'Request failed with status code ' + response.status,
response.config,
null,
response.request,
response
));
}
};
*/
// 由status来判断那些请求当做成功,那些当做失败
settle(resolve, reject, response);
request = null;
};
// 当一个请求终止时 abort 事件被触发
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(createError('Request aborted', config, 'ECONNABORTED', request));
request = null;
};
// 当请求遇到错误时,将触发error 事件。
request.onerror = function handleError() {
// Real errors are hidden from us by the browser
// onerror should only fire if it's a network error
reject(createError('Network Error', config, null, request));
request = null;
};
// 当进度由于预定时间到期而终止时,会触发timeout 事件。
request.ontimeout = function handleTimeout() {
var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(createError(timeoutErrorMessage, config, 'ECONNABORTED',
request));
request = null;
};
//添加xsrf标头只有在标准浏览器环境中运行时才能执行此操作。
if (utils.isStandardBrowserEnv()) {
var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
cookies.read(config.xsrfCookieName) :
undefined;
if (xsrfValue) {
requestHeaders[config.xsrfHeaderName] = xsrfValue;
}
}
// 向请求中添加headers
if ('setRequestHeader' in request) {
utils.forEach(requestHeaders, function setRequestHeader(val, key) {
if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
//如果数据未定义,则删除内容类型
delete requestHeaders[key];
} else {
//否则,将头添加到请求中
request.setRequestHeader(key, val);
}
});
}
// 配置请求是否支持携带cookie
if (!utils.isUndefined(config.withCredentials)) {
request.withCredentials = !!config.withCredentials;
}
// 将responseType添加到请求中
if (config.responseType) {
//..
}
// 控制请求进度
if (typeof config.onDownloadProgress === 'function') {
request.addEventListener('progress', config.onDownloadProgress);
}
// 上传事件的兼容处理,增加健壮性
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
// 这里时取消请求的处理逻辑,参考[带着问题加深理解]
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}
request.abort();
reject(cancel);
request = null;
});
}
if (!requestData) {
requestData = null;
}
// 发送 HTTP 请求
request.send(requestData);
});
};
代码很长,即使删除了部分代码,依然后150+行,看到这里了就仔细去分析一些吧,快结束了(我也快写不动了😭)
这个文件做了什么呢
- 返回
Promise
- 规范了请求字段(data,headers,url等等)
- 一些方法的边缘探测处理(
getAllResponseHeaders
) - 方法校验、状态码,数据流向处理(由status决定),健壮性增强
- 发起了一个xhr请求
至此,已全部结束主要源码的分析,里面一些工具类函数和辅助函数也不错,也有学习的地方,建议大家看看。
那么我们梳理一些大概的流程
梳理小结
- 初始化时创建实例(
createinstance
),此过程中为实例定义和继承了一些方法 取消请求(Cancel
、CancelToken
、isCancel
)/错误处理(isAxiosError
)/ 请求方法(request
)/请求语法糖(get
,post
,delete
等等) - 将拦截器里的回调(
request_fullfilled
,request_rejected
,response_fullfilled
,response_rejected
)装入执行链chain
。有点像Promise的初始化 - 调用
disapatchRequest
,利用适配器Adapter
发起请求。 - 对请求数据进行转换(xhr初始化),请求拦截器(如果存在)请求配置合并,并返回配置。
- 发出请求(
dispactchRequest
),将响应数据返回给响应拦截器(如果存在)。这里存在对数据的二次加工(validateSatatus
区分是否成功,data
不同,失败/成功回调函数接=接受的参数) - 响应拦截器(如果存在)对数据进行二次加工。返回给
axios.then()
的回调函数的参数。 - END!
⚾带着问题加深理解
1.取消请求如何实现
先看看怎么用,有两种方式。
可以使用 CancelToken.source
工厂方法创建 cancel token
,像这样:
var CancelToken = axios.CancelToken;
var source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function(thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
还可以通过传递一个 executor
函数到 CancelToken
的构造函数来创建 cancel token
:
var CancelToken = axios.CancelToken;
var cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// 取消请求
cancel();
那种更优雅,用着更方便呢,个人认为时第二种。理解直观,使用优雅 (代码少)😂
取消实现主要执行了传入执行器 executor
的参数函数(cancel)
接着看看源码,前面在分析适配器时注释里提了一下,当中有涉及到相关代码
🧾adapters/xhr.js
// 代码很好理解,如果配置了cancelToken 那么就执行 request.abort() 取消请求
if (config.cancelToken) {
config.cancelToken.promise.then(function onCanceled(cancel) {
// 在request 为null时, 直接返回,取消没有意义
if (!request) {
return;
}
request.abort();
reject(cancel);// Promise状态更改为rejected,并将cancel作为错误捕获回调参数!
// Clean up request
request = null;
});
}
上面的 reject(cancel)
的 cancel 是什么?接着来看 lib/cancel 文件夹
🧾cancel/Cancel.js
function Cancel(message) {
this.message = message;
}
// 重写toString方法
Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};
🧾cancel/CancelToken.js
var Cancel = require('./Cancel');
//cancelTOken 接受一个执行器 executor 函数作为参数。
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
// 将Promise的执行权暴露出去
resolvePromise = resolve;
});
var token = this;
// 传入的cancel函数触发cancelToken.promise.then 见xhr.js
executor(function cancel(message) {
if (token.reason) { // 已经取消过
return;
}
token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}
// 如果请求过了(完成)就把 Cancel 当错误抛出。
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};
// 这个不过是上面第一种方式的补充实现。
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
cancel 其实就是 token.reason
!
总结:
- 传入配置项 cancelToken 一个函数(executor),此函数又接受一个参数(cancel, 也为函数),才参数暴露出去给外部调用(一旦调用, 就执行
request.abort()
)。 - 上面
executor(function cancel(){})
里面的cancel
函数就是调用方式第二种的c
, 当c
调用才会触发promise.then()
的回调函数(第一个). axios.get().then().catch(msg => {})
当发生请求取消,取msg就会调用Cancel.prototype.toString
方法
由不住再想说一句
“卧槽!还可以这样写,绝啊!“
2.拦截器如何实现的
其实上面拦截器也说的差不多了,这里做个总结吧
主要代码就是 🧾Core/Axios.js 里面一段
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);
//调用拦截器自定义方法forEach,参考后面[拦截器]
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
// interceptor 就是我们传入拦截器的两个函数, 然后插入chain数组开头
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
// 向chain数组末尾添加元素
chain.push(interceptor.fulfilled, interceptor.rejected);
});
//while循环一步步利用then链向下执行
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
/*
相当于
promise.then(request_fullfilled, request_rejected)
.then(dispatchRequestFunc, undefined)
.then(response_fullfilled, response_rejected)
*/
}
总结:
保持这样一个数组 ,请求拦截器的参数始终向头部添加,响应拦截器的参数始终想尾部添加!
[...,请求拦截器成功函数, 请求拦截器失败函数, XHR请求, undefined,响应拦截器成功函数, 响应拦截器失败函数,...]
然后 用 Promise.then 一个个串起来执行。请求拦截器的配置始终能更改XHR请求, XHR请求的数据也始终会返回到响应拦截器的参数(成功/失败函数)里。这也就解释了
- 请求拦截器的成功函数里返回 Promise 会挂起请求
- 响应拦截器的成功函数里放回 Promise 请求不会有响应值。
大概这个样 | 【掘金】逐行解析Axios源码⇲
3.请求配置的优先级
大家知道,core/defaults.js 里面有配置,axios.create(config)
也有配置, 响应拦截器里也有配置,那么这些配置的优先级是如何的呢?来看看那些地方执行了配置合并
🧾core/Axios.js
Axios.prototype.request = function request(config) {
config = mergeConfig(this.defaults, config);
}
// 没看到调用
Axios.prototype.getUri = function getUri(config) {
config = mergeConfig(this.defaults, config);
};
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
return this.request(mergeConfig(config, ...));
}
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
his.request(mergeConfig(config, ...));
}
🧾axios.js
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
这里我就直接出结论了哈, 优先级由小到大
defaults里config << 初始化config(axios.create(cfg)/axios.get(cfg)) << 请求拦截器参数返回的config.
其实 headers
自定义配置也可以在这里解释!就是一个合并的事
4.请求超时如何实现
当请求时间过程没有响应,超出我们设定的时间(ms),并且进入then
xhr.js
var request = new XMLHttpRequest();
request.timeout = config.timeout;
// 当进度由于预定时间到期而终止时,会触发timeout 事件。
request.ontimeout = function handleTimeout() {
var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
if (config.timeoutErrorMessage) {
timeoutErrorMessage = config.timeoutErrorMessage;
}
reject(createError(timeoutErrorMessage, config, 'ECONNABORTED',
request));
request = null;
};
其实就是重写 ontimeout
方法,重置 request
。 而 timeout
是 XMLHttpRequest
对象的 原生方法⇲。
待补充
后面发现了再去看吧,也希望各位看客补充!
最后
⚾总结
一图总览 【掘金】学习 axios 源码整体架构,打造属于自己的请求库⇲
— 图是什么画的?
—了解一下 xmind
参考
除了上面提到的 链接,还参考了如下文章。站在别人的肩膀上可以看的更高。
⇲ Axios 源码解析
⇲ 深入浅出 axios 源码
⇲ 如何实现一个HTTP请求库——axios源码阅读与分析
⇲ Axios源码分析