axios是一个基于promise的http库,支持浏览器和node端,最近我在做beauty-we的api设计,研读一个成熟的http库势在必行,axios功能完整、api简洁、注释清晰,再适合不过,现在就让我们开始吧~
准备工作
clone项目,打开dist文件夹,里面有axios.js和axios.min.js,看一下axios.js的第一段:
(function webpackUniversalModuleDefinition(root, factory) {
if (typeof exports === 'object' && typeof module === 'object') {
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
define([], factory);
} else if (typeof exports === 'object') {
exports.axios = factory();
} else {
root.axios = factory();
}
})(this, function() {...我是主代码部分...});
最外层是一个立即执行函数(IIFE),传入参数this和构造函数factory,区分了4种情况:
- exports和module都存在时,cmd模式;
- define和define.amd都存在时,amd模式;
- 只有exports存在时;
- 既不是cmd也不是cmd模式时,挂载在传入的宿主对象上。
我比较了其他3个类库的处理(underscore/jquery/vue):
underscore
(function() {
var root = typeof self == 'object' && self.self === self && self ||typeof global == 'object' && global.global === global && global ||this ||{};
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
// ....这里是主代码部分...
if (typeof define == 'function' && define.amd) {
define('underscore', [], function() {
return _;
});
}
}());
jquery
(function( global, factory ) {
if ( typeof module === "object" && typeof module.exports === "object" ) {
module.exports = global.document ?
factory( global, true ) :
function( w ) {
if ( !w.document ) {
throw new Error( "jQuery requires a window with a document" );
}
return factory( w );
};
} else {
factory( global );
}
if ( typeof define === "function" && define.amd ) {
define( "jquery", [], function() {
return jQuery;
} );
}
})(typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
// ...这里是主代码部分
})
vue
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Vue = factory());
}(this, (function () {
// ...这里是主代码部分...
}))
4个库中,
- underscore和axios考虑了4种环境情况,vue和jquery无视了exports存在但没有module的情况(这种情况什么时候出现我不清楚,知道的人求指出);环境判断vue用了3目运算符,看起来又干净又漂亮(其实这好像并不重要,压缩后大家都一样~);对amd的支持underscore和jquery都放在了代码的尾部,打补丁的痕迹好明显;
- jquery强调了document一定存在,否则会抛出错误,axios和vue只是传入了this,underscore则认真区分了browser的window,server端的global,其他端的this,自创了一个self统一之,这意味着后三者可以存活在更普遍的环境中(比如axios in web worker,比如weex),underscore的做法最严谨;
- 除了underscore,其他3者都把主代码放在了IIFE的参数里,而underscore是直接写在IIFE里,我个人还是喜欢用那个叫factory的函数。
看整体
啰嗦了那么多之后,正式开始看axios啦。根据package.json里的npm run dev的指向,我们打开lib文件夹里的axios.js,这就是全局入口了。
./lib/axios.js只做了两件事:
- 创建一个axios对象
- 暴露一些拓展方法出去
先看axios对象的创建:
var axios = createInstance(defaults);
function createInstance(defaultConfig) {
// 一切的开始,new一个实例出来
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);
utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
return instance;
}
创建实例axios用了4步,第2步开始就比较奇怪了:
var instance = bind(Axios.prototype.request, context);
看一下bind函数:
function bind(fn, thisArg) {
return function wrap() {
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
return fn.apply(thisArg, args);
};
}
它返回一个wrap()函数,其中执行了参数1的函数,绑定了参数2的上下文,也就是说,instance是一个叫wrap的函数,它里面执行了Axios原型对象上的request方法,同时绑定了最开始创建的那个实例的上下文;
再看下一行:
utils.extend(instance, Axios.prototype, context);
看一下extend函数:
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
它承担了两个任务,当参数3存在时,它循环参数2,把参数2 上的属性是函数类型的都用bind的方式挂载到参数1身上;否则的话,它实现了简单的把参数2的复制给参数1;
那么看最后一行就知道了,3-4实现的是把Axios原型上的函数属性都挂载到instance,这是第2行的一个循环版本,最后把axios实例复制给instance,形成一个真正的instance,此时,这个instance具备Axios实例的静态属性(复制来的),Axios.prototype上所有函数方法(通过bind,返回一个wrap(),里面调用Axios.prototype上的方法),instance本身还是一个wrap版本的Axios.prototype.request方法。
这时我好想问一个:为什么这么做。。
思考好久找到突破口,就是最奇怪的最后一点,axios是一个wrap函数,包含了request,这其实是因为axios支持的两种调用方式:axios({config}).then()
和axios.get('url').then()
前者确定了axios自身作为一个函数的特性,同时,它还需要具有get/post等一堆方法,于是我翻到的commit最初的版本是:
var defaultInstance = new Axios();
var axios = module.exports = bind(Axios.prototype.request, defaultInstance);
这里还少了把defaultInstance上的静态属性复制给axios的过程,当时Axios上的静态属性还很少,只有defaults,被手动挂载给axios:axios.defaults=defaults;
,
随着Axios添加了InterceptorManager拦截器,进化成了第二版本:
var defaultInstance = new Axios();
var axios = module.exports = bind(Axios.prototype.request, defaultInstance);
axios.defaults=defaults;
axios.interceptors = defaultInstance.interceptors;
但是有人发现了一个bug,因为axios自带create方法,可以自定义一个实例,而create内写的却是极度简陋的:
axios.create = function (defaultConfig) {
return new Axios(defaultConfig);
};
大家发现axios.create出来的和axios比较后api少很多啊,于是终极版本的构建函数createInstance诞生了:
function createInstance(defaultConfig) {
// 一切的开始,new一个实例出来,然而考虑到它不是最终对象,只能称之为上下文
var context = new Axios(defaultConfig);
// 这是最后返回的实例,本身是一个request函数
var instance = bind(Axios.prototype.request, context);
// 为了拓展性,一口气把原型对象上所有的方法都拷一份
utils.extend(instance, Axios.prototype, context);
// 把实例的静态属性复制过来
utils.extend(instance, context);
return instance;
}
终于创建完axios对象,接下来是暴露一些api出去:
// Factory for creating new instances
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// Expose Axios class to allow class inheritance
axios.Axios = Axios;
axios.all = function all(promises) {
return Promise.all(promises);
};
axios.spread =function spread(callback) {
return function wrap(arr) {
return callback.apply(null, arr);
};
}
axios.Cancel...
axios.CancelToken...
axios.isCancel...
入口文件的分析就到此结束了。
core Axios
开始研究核心代码Axios这个类;
首先是构造函数:
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
request: new InterceptorManager(),
response: new InterceptorManager()
};
}
看完上面的内容大家应该有点印象,axios上挂了defaults和interceptors,defaults是默认的config配置,interceptors顾名思义就是拦截器,目测包含了request和response两种类型。
接下来看个简单的部分:
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
}));
};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
/*eslint func-names:0*/
Axios.prototype[method] = function(url, data, config) {
return this.request(utils.merge(config || {}, {
method: method,
url: url,
data: data
}));
};
});
axios把7种请求都转向了request方法,根据是否要带data写了2个循环,看起来还挺简洁的。
接下来看最核心的request代码,这里的处理很巧妙很优雅:
Axios.prototype.request = function request(config) {
// ...我是很多config处理...此时不关心
// 创建了一个事件数组,第一位的是发送请求的动作,这是默认状态
var chain = [dispatchRequest, undefined];
// 创建了一个promise对象
var promise = Promise.resolve(config);
// 循环request拦截器,有的话就添加到chain的头部
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});
// 循环response拦截器,有的话就添加到chain的尾部
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});
// 利用while循环,执行完所有事件
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
// 返回最终的promise对象
return promise;
}
核心axios对象到此已看完,下一节计划分析dispatchRequest---adapter,真正发送请求的部分。喜欢就点个赞吧
参考文章: