为何要阅读源码?用尤大的话:提升自己的行业竞争力。本文以Axios为例,带你一下学习源码。之所以选择Axios,因为它是一款非常流行的处理http请求的库,前端几乎人人在用,复杂度适中且有很好的注释解释。
姿势很重要
正式开始前,先普及一下源码阅读的正确姿势:
- 去
Github
fork一个你想学习的项目(如果你不知道Github,那你不用往下看了) - 在fork的项目下建一个学习分支
- 熟悉、理解项目结构
- 开始阅读(最好遵循一定的顺序)
- 在源代码里添加你自己的理解或标记
- 提交学习记录到学习分支
再有,就是要有一个平常的心态,没有人天生就是代码高手;同时要有正确的学习理念,那就是:
- 你是学习人家的设计思想,而不是全盘吸收,这玩意真不值得你全部背下来
- 对于那些非常巧妙的点,一定要重点关注,确保理解和吸收。
- 做一些练习巩固你的理解
好,你准备好了吗?进入正题!
先别慌,瞄一眼很重要
先看他的package.json文件,了解两个关键信息:
- 入口文件是
index.js
- 打包命令是
npm run build
打开terminal键入命令npm install
、npm run build
然后进入dist目录查看axios.js
文件:
这是最后一行的行号–2297,其实我想说非压缩的版本只有2000多行,所以确实不复杂,我没骗你的!
其次再瞄一眼目录结构:
lib
目录是存放源码的,像其他目录如 examples
、sandbox
、test
等是起辅助作用的,dist
是编译包的存放目录,所以我们主要关注的部分将是lib
目录。
ok,瞄完了!
所以我们知道了应该从入口index.js
开始,源码都在lib
目录,不懂可以看注释文档或README.md
从入口文件开始
入口文件内容很直接:module.exports = require('./lib/axios');
,所以间接入口是lib目录下的axios.js
内容也很简单:
- 创建一个Axios的实例
- 往上面挂几个方法或属性
- 返回该实例
'use strict';
//引入省略。。。
/**
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
* @return {Axios} A new instance of Axios
*/
// 创建Axios的一个实例的工厂函数
function createInstance(defaultConfig) {
//省略了
}
// 执行示例创建
// Create the default instance to be exported
var axios = createInstance(defaults);
// 暴露自己的爹
axios.Axios = Axios;
// 把创建自己的工厂函数也暴露出去
axios.create = function create(instanceConfig) {
return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
// 挂载几个有用的方法
// 省略。。。
axios.xxx = yyyy;
// 最后导出,o了
module.exports = axios;
// Allow use of default import syntax in TypeScript
module.exports.default = axios;
复制代码
现在,你明白了Axios实例是通过createInstance()
创建的,可以通过axios.create()
间接调用它,并且该库的作者非常贴心,默认为你创建好并导出了,你直接 require('axios')
就可以使用了。
这样阅读是不是有点走马观花?我觉得一点也不,这里不值得我们下狠功夫!
请求是如何发起的?
我们知道,可以通过调用axios.get|post
发起请求,然后处理返回的promise
对象即可对响应的数据做处理。那么,这一切的背后原理是怎样的?
首先这些方法在哪里定义?Axios
原型对象:
// lib/core/Axios.js
// 遍历方法数组给原型对象添加方法
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
}));
};
});
复制代码
接着我们看到会返回this.request
函数的调用返回值,它当然是一个promise
对象:
/**
* Dispatch a request
*
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(config) {
// 省略了无数行,这里他们不重要
try {
//创建请求的promsie对象
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
// 省略了无数行,这里他们不重要
//返回一个promise对象
return promise;
};
复制代码
所以我们断定dispatchRequest
这哥们是个关键角色:
/**
* Dispatch a request to the server using the configured adapter.
*
* @param {object} config The config that is to be used for the request
* @returns {Promise} The Promise to be fulfilled
*/
module.exports = function dispatchRequest(config) {
// 省略了无数行
// 获取adapter
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
// 转换相应数据的,这里不重要,省略
return response;
}, function onAdapterRejection(reason) {
// 转换相应数据的,这里不重要,省略
return Promise.reject(reason);
});
}
复制代码
可以基本断定直接负责请求的就是这个adapter
,由于axios
是一个可以运行在web和node里的http client,所以这里的adapter
也分web和node两种,分别对应的文件是adapters/xhr.js
和adapters/http.js
:
// lib/defaults.js
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');
}
复制代码
所以我们的目光又来到了xhr.js
:
function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var request = new XMLHttpRequest();
function onloadend() {
if (!request) {
return;
}
// 省略。。。
//将resolve和reject托管给settle函数处理
settle(resolve, reject, response);
// Clean up request
request = null;
}
if ('onloadend' in request) {
// Use onloadend if available
request.onloadend = onloadend;
} else {
// Listen for ready state to emulate onloadend
request.onreadystatechange = function handleLoad() {
if (!request || request.readyState !== 4) {
return;
}
// 省略了一部分处理边界的代码
setTimeout(onloadend);
};
}
request.onabort = function(){
// 省略
}
request.onerror = function(){
// 省略
}
request.onerror = function(){
// 省略
}
})
}
复制代码
可以看到在xhr的onload事件处理函数里,promise的resolve
和reject
函数会被传递给settle
函数,由它来决定resolve
、reject
的调用。这俩函数如果不被调用,任你怎么axios.get(...).then()
都不会得到预期的效果。
相信settle.js
应该没几行代码了:
module.exports = function settle(resolve, reject, response) {
// 对响应做校验,通过则调用resolve 否则reject
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
));
}
};
复制代码
好了,流程看完了,再来梳理一遍http请求的执行栈(流程),以get方法为例: