介绍
axios 作为最知名的请求库,大量应用在web
,node
环境中,有很多有趣的特点。今天就学习一下他的源码,看看有哪些宝藏思想 💎
❤ 工厂实例 与 调用
axios.create([config])
const instance = axios.create({
baseURL: "https://some-domain.com/api/",
timeout: 1000,
headers: { "X-Custom-Header": "foobar" },
});
实例方法
以下是可用的实例方法。指定的配置将与实例的配置合并。
- axios#request(config)
- axios#get(url[, config])
- axios#post(url[, data[, config]])
- .....
入口文件
lib > axios.js
平时我们是直接使用的axios.get
本质是调用了 createInstance的返回值(即 instance) 上 的方法
当调用axios的实例方法 axios.create
时,是调用了 instance.create(即createInstance)
👨 当我使用instance.get
的时候,就和原来的axios.get
没有差别了
instance.create
接受的参数instanceConfig
与默认参数defaultConfig
进行合并,然后产出一个 instance
,这就又回到了平时我们最常使用的那种方法
不得不说,很巧妙 👍
🧐 函数 or 对象
axios 有两种用法,一种是函数调用,一种是对象方式
- 函数用法
axios({url:"http://xxx", method:"get",data:{ name:"zs" } })
- 对象用法
axios.get("http://xxx")
既可以当作函数来调用,也可以当作对象
原理就在上面的那个createInstance
方法里面
前置知识
bind
先看 bind
方法
作用和 Function.prototype.bind
方法目的是一样的,绑定 this 指向
extend
utils.extend
第一个是原对象(source),第二个是要遍历属性的对象(target),第三个是 原对象的 this 指向
可以简单理解成把 target
身上的属性 向 source
身上添加
可以简化成
ok,前置知识讲到这里,现在说说为什么可以既可以当函数,也可以当作对象
💣大家都知道,在 js
的世界里,万物皆对象,函数也不例外
- 绑定 this,生成初始函数
把
request
方法中的this
绑定为Axios
,因为在request
中有用到this
同时生成一个 函数(instance)
// 和下文效果没有差别
const instance = bind(Axios.prototype.request, context);
// const instance = Axios.prototype.request.bind(context);
- 添加方法
前面说了,万物皆对象,在对象身上绑定几个方法不过分吧, 传入 context 是为了保证 this 指向
extend(instance, Axios.prototype, context, { allOwnKeys: true });
向 instance
身上 加上 Axios.prototype
身上的方法,比如get/post
等等,
此时,instance
就可以使用 instance.get / instance.post
了
Axios的原型方法:
- 添加属性
extend(instance, context, null, { allOwnKeys: true });
经过这一步,instance
还把Axios
的实例属性也给拷贝了一份
🎇 终极形态:
小结
当你使用 axios
函数式的时候,你调用的是Axios.prototype.request
当你使用 axios
的对象形式的时候,你调用的是Axios.prototype
的实例方法
链式调用 - 请求响应拦截器
在请求或响应被 then 或 catch 处理前拦截它们
先看使用
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
用过axios的程序员应该对拦截器都不陌生吧,可以对 请求的参数 / 返回的结果 做进一步的统一处理
那么是怎么做到的?🧐
答案就在这个文件里
简化代码
interceptor.fulfilled
可以理解成axios.interceptors.request.use
传入的第一个函数
interceptor.rejected
可以理解成axios.interceptors.request.use
传入的第二个函数,有可能是 undefined
// 请求拦截数组
const requestInterceptorChain = [];
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor){
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
})
// 响应拦截数组
const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
现在 requestInterceptorChain
和 responseInterceptorChain
的数据结构类似于
requestInterceptorChain = [
// 第二个请求拦截
function(config){ },
function( error){ },
// 第一个请求拦截,如果传入第二个函数的话
function(config){ },
undefined
];
🎗️ Tip:
请求拦截器requestInterceptorChain
是unshfit
后面的插在队头
响应拦截器responseInterceptorChain
是push
插入队尾
后面的是重点 🚀
dispatchRequest.bind(this)
是返回一个Promise
,可以直接当作Promise.resolve
看待
🔥小技巧:
由于requestInterceptorChain / responseInterceptorChain
是一个数组, 如果直接使用unshift / push
,那么直接插入的是一个数组,需要使用apply
方法把参数分开插入
// const chain = [dispatchRequest.bind(this), undefined];
const chain = [Promise.resolve(config), undefined]
chain.unshift.apply(chain, requestInterceptorChain);
chain.push.apply(chain, responseInterceptorChain);
len = chain.length;
promise = Promise.resolve(config);
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
chain 最终的结构大概是[请求拦截器2,请求拦截器1,发送请求,响应拦截器1,响应拦截器2]
以Promise
作为流转的中间过程,通过 Promise 的 then 链, 这样就可以实现不断地链式调用
做一个简化版,加深理解
对对象
config = { name :'zs'}
通过请求拦截器添加属性age
和address
如果age
大于 18,则通过 响应拦截器 返回成年人
,如果没有,返回未成年人
const config = {
name: "zs"
}
// 第一个请求拦截器
const firstRequestInterceptor = [
function (config) {
config.age = 20;
return config
}, function (err) {
Promise.reject(err)
}
]
// 第二个请求拦截器
const twoRequestInterceptor = [
function (config) {
config.address = "杭州";
return config
}, undefined
]
//😶 第一个响应拦截器
const firstResponseInterceptor = [
function (config) {
// { name:"zs",age:20,address:"杭州" }
if(config.age > 18){
return "成年人"
}else {
return "未成年人"
}
},
undefined
]
// 模拟 unshift
const requestInterceptorChain = [
...twoRequestInterceptor, ...firstRequestInterceptor
];
const responseInterceptorChain = [
...firstResponseInterceptor
]
const chain = [Promise.resolve(config), undefined];
chain.unshift.apply(chain,requestInterceptorChain);
chain.push.apply(chain,responseInterceptorChain);
len = chain.length;
let promise = Promise.resolve(config);
let i = 0;
while (i < len) {
promise = promise.then(chain[i++], chain[i++]);
}
promise.then(res=>{
console.log(res,"res")
})
通过 promise
链条把chain
中的函数串起来, then
中的函数的返回值作为下一个 then
函数的初始值,这样可以对 config
不断 添加属性,响应同理
❤ Promise 的发布订阅 - 取消请求
在文件
lib > cancel > CancelToken.js
中
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
// executor 函数接收一个 cancel 函数作为参数
cancel = c;
})
});
// 取消请求
cancel();
显而易见,这个方法用来取消请求的,在类CancelToken
中的接收一个函数,这个函数的参数c
赋值给外部变量cancel
,执行 cancel
来达到取消请求的目的
class CancelToken {
constructor(executor){
let resolvePromise;
// ....
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
// ....
executor(function cancel(message, config, request) {
resolvePromise(message);
});
}
}
那么 axios
中的参数cancelToken
是怎么和 CancelToken
类 扯上关系的呢?
原因在 lib>adapters>xhr.js
(xhr 发送请求函数) 中
如果config
中有cancelToken
,那么就调用 cancelToken.subscribe
方法
看cancelToken.subscribe
方法,在文件lib > cancel > cancelToken.js
中
CancelToken
类的一个实例方法
那么 CancelToken
这个类就有一个this._listerers
的onCancel
数组了
那么执行外界变量cancel
的时候,会发生什么呢
🚩这就是有趣的地方了
// CancelToken 类中
let resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
const token = this;
this.promise.then(cancel=>{
let i = token._listeners.length;
while (i-- > 0) {
token._listeners[i](cancel);
}
token._listeners = null;
})
executor(function cancel(message, config, request) {
resolvePromise(message);
});
它会依次调用_listeners
中存储的onCanceled
方法,类似于发布订阅
简化版
代码贴在这,有兴趣可以试试
function xhr(config) {
let conf = {
name: "zs",
};
function onCancel(message) {
console.log(message, "错误消息");
conf.name = null;
}
if (config.CancelToken) {
// 把 onCancel 注入到 CancelToken 中
config.CancelToken.subscribe(onCancel);
}
// 模拟异步
setTimeout(() => {
console.log(conf, "conf");
});
}
class CancelToken {
constructor(executor) {
// 现在 resolvePromise 控制着 this.promise 的执行,
// resolvePromise 执行,this.promise 就会执行
let resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
const token = this;
this.promise.then(message => {
token._listeners.forEach(fn => {
fn(message);
});
});
// 外部传入的函数 c == cancel
executor(function cancel(message) {
resolvePromise(message);
});
}
subscribe(listener) {
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
}
}
// 模拟使用
let cancel;
xhr({
CancelToken: new CancelToken(function executor(c) {
cancel = c;
}),
});
cancel("取消请求");
元辅音 - 判断类型
在文件
lib > helpers > validators
中
const validators = {};
['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach((type, i) => {
validators[type] = function validator(thing) {
return typeof thing === type || 'a' + (i < 1 ? 'n ' : ' ') + type;
};
});
validators
会是这样的数据结构
使用
console.log(validators.number(2)) // true
console.log(validators.boolean(2)) // 'a boolean'
本身平平无奇,有点好奇这个 👉i < 1 ? 'n ' : ' '
是什么意思?
console.log(validators.boolean(2)) // 'a boolean'
console.log(validators.object(2)) // an object
原来是 an
放在元音音素前面,a
放在辅音音素前面, o
是元音音素,其余都是辅音音素,细节
总结
axios
作为一个经久不衰的库,必然有他的过人之处,通过阅读它的源码,确实可以学到很多思想
通过阅读源码,不仅可以对这个库的使用有更深刻的理解,也能够与大佬隔空对话,学习他们的思想,毕竟一般大量使用的库都是有其独特优势。而且他山之石可以攻玉,当遇到类似的问题的时候,可以从脑海里检索出最优解,毕竟输出是由输入决定的
不仅可以看他们的逻辑思路,也可以思考文件分割设计、代码技巧、代码风格、语法规范,是否有可取之处,为什么这么设计,自己有没有更好的想法,如果有更优解,提个pr
也是不错的