Fork me on GitHub

axios源码阅读

 没事读读码…

  axios在业务中请求用得比较多了,这个周末就花点时间阅读下源码,先进github开启sourcegraph插件,找到核心实现目录:

  那从哪里开始慢慢看比较好呢?我个人其实比较倾向从工具方法里入手,可以学习其中的编码思路,发现平常自己实现相同功能容易忽略的细节。了解了其中的工具方法作用后,对我们后续盘核心代码的逻辑也会轻松不少。

helpers

  该文件目录下主要是一些对我们发送请求时,拼接URL、处理参数的工具方法。

bind.js

1
2
3
4
5
6
7
8
9
10
11
'use strict';

module.exports = 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);
};
};

  其实就是在ES5下,实现了一个bind,返回一个闭包,最后返回绑定this的调用返回结果。

adapters

  根据文件目录,及README.md,该模块主要进行浏览器端及Node端的网络请求兼容,它会处理成一个request请求进行dispatch,并且当response返回后,处理返回一个Promise。

cancel

  这个模块进行了基本的Cancel对象封装,主要用于判断请求是否被Cancel以及如何进行请求的Cancel。

Cancel.js

  构造了一个Cancel函数,初始化message信息,原型链上重载了toString方法以及添加了__CANCEL__标记变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
'use strict';

/**
* A `Cancel` is an object that is thrown when an operation is canceled.
*
* @class
* @param {string=} message The message.
*/
function Cancel(message) {
this.message = message;
}

Cancel.prototype.toString = function toString() {
return 'Cancel' + (this.message ? ': ' + this.message : '');
};

Cancel.prototype.__CANCEL__ = true;

module.exports = Cancel;

CancelToken.js

  CancelToken是一个用于进行请求取消操作的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
'use strict';

var Cancel = require('./Cancel');

/**
* A `CancelToken` is an object that can be used to request cancellation of an operation.
*
* @class
* @param {Function} executor The executor function.
*/
function CancelToken(executor) {
if (typeof executor !== 'function') {
throw new TypeError('executor must be a function.');
}

var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});

var token = this;
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}

token.reason = new Cancel(message);
resolvePromise(token.reason);
});
}

/**
* Throws a `Cancel` if cancellation has been requested.
*/
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
if (this.reason) {
throw this.reason;
}
};

/**
* Returns an object that contains a new `CancelToken` and a function that, when called,
* cancels the `CancelToken`.
*/
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};

module.exports = CancelToken;

  通过源码阅读我们可以看出,CancelToken也是一个构造函数,它接受一个执行函数executor,内部通过一个Promise实例控制,比较有趣的是它将Promise状态改变的回调函数执行句柄提出,并在executor中执行后再触发。并在执行函数触发时,首次执行会调用之前Cancel的构造函数,并将生成的实例赋值给当前上下文的reason属性。在下一次再触发时,若已有reason内容,则不再执行该函数。

  除此之外,在axios中真正使用CancelToken往往不是直接通过new构造,而是使用函数的静态方法source,它通过注入一个函数的形式,从CancelToken内部拿到了真正进行取消动作的cancel函数,并将其赋值给了外层source函数内部的cancel变量,最终返回了这个CancelToken实例以及与其匹配的取消方法,形成一个闭包。

isCancel.js

  判断任务是否已被取消,从构造上来说,它的入参是Cancel函数构造的实例,返回值的处理也比较巧妙,运用!!真值处理,因为如果valueundefined,返回的就是undefined,真值处理会进行布尔值转换。

1
2
3
4
5
'use strict';

module.exports = function isCancel(value) {
return !!(value && value.__CANCEL__);
};

axios是怎么做请求取消的?

  了解了以上构造函数实现后,我们知道了核心是CancelToken.source方法以及CancelToken实例原型链上对应的__CANCEL__属性。再看看真实应用的例子from README.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log('Request canceled', thrown.message);
} else {
// handle error
}
});

axios.post('/user/12345', {
name: 'new name'
}, {
cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

  从使用demo上,我们知道在axios进行请求时,我们的CancelToken实例作为参数传入,而取消句柄则在外部被我们开发者在对应业务场景消费,简单来说就是我们可以决定何时取消。

  那么实际我们的axios实例是如何运用以上的token和对应的cancel呢?

  1. Axios构造函数实现核心请求方法request,本质上不同的请求方法(getputpost等)最终都会调用这个request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Provide aliases for supported request methods
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
}));
};
});

  2. request中间的请求体会通过interceptors形成一个中间件进行处理,这里我们先不看具体中间件做了什么动作,聚焦到里面实际发起请求的函数dispatchRequest,代码中的adapter其实就是浏览器和Node端对应真实发起请求的方法封装,它们最终会返回一个Promise。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 省略部分 ...
var adapter = config.adapter || defaults.adapter;

return adapter(config).then(function onAdapterResolution(response) {
throwIfCancellationRequested(config);

// Transform response data
response.data = transformData(
response.data,
response.headers,
config.transformResponse
);

return response;
}, function onAdapterRejection(reason) {
if (!isCancel(reason)) {
throwIfCancellationRequested(config);

// Transform response data
if (reason && reason.response) {
reason.response.data = transformData(
reason.response.data,
reason.response.headers,
config.transformResponse
);
}
}

return Promise.reject(reason);
});

  在这个Promise的回调中,我们会判断config配置是否有cancelToken属性,即是否配置了取消请求的方法,如果有,则检查其中是否已经存在了取消的reason属性,这个reason属性根据前文,它是一个Cancel对象,内部是我们外部调用取消方法传入的msg。即如果这个cancelToken实例此时存在reason了,它就会抛出这个内部的reason即Cancel对象。

1
2
3
4
5
6
7
8
/**
* Throws a `Cancel` if cancellation has been requested.
*/
function throwIfCancellationRequested(config) {
if (config.cancelToken) {
config.cancelToken.throwIfRequested();
}
}

  3. adapter我们就以浏览器端的实现xhr.js文件来看,可以看出浏览器端就是去构造一个XMLHttpRequest,在真实发送send前,判断config中是否有cancelToken,有则以cancelToken内部的promise来进行异步控制。通过前文的了解我们知道,在具体业务场景我们调用cancelToken匹配的cancel方法进行请求终止,其实就是将CancelToken内部的promise进行resolve并使得在adapter中进入异步等待回调的promise立马回调,将XMLHttpRequest的请求实例通过abort方法终止。然后这个axios请求的Promise将会reject,里面的属性就是cancelTokenreason。同时释放request内存。实际上在请求的各个阶段结束后,如错误、终止、完成都会清空request指向,这也是CancelTokenpromise回调中发现request已经阶段完成就啥都不做的判断逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
module.exports = function xhrAdapter(config) {
return new Promise(function dispatchXhrRequest(resolve, reject) {
var requestData = config.data;
var requestHeaders = config.headers;
var request = new XMLHttpRequest();
// 省略...
request.onreadystatechange = function handleLoad() {
// ...
request = null;
};
request.onabort = function handleAbort() {
if (!request) {
return;
}
reject(createError('Request aborted', config, 'ECONNABORTED', request));
// Clean up request
request = null;
};
// Handle low level network errors
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));

// Clean up request
request = null;
};
// Handle 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));

// Clean up request
request = null;
};
if (config.cancelToken) {
// Handle cancellation
config.cancelToken.promise.then(function onCanceled(cancel) {
if (!request) {
return;
}

request.abort();
reject(cancel);
// Clean up request
request = null;
});
};
request.send(requestData);
});
};

utils.js

  该文件下,主要是一些判断类型的工具方法:

  一个基本的判断类型思路:

1
2
3
4
5
6
// 获取toString 方便后续调用 避免每次都用.重新查找获取
var toString = Object.prototype.toString;
// 通过转化字符串的特点进行类型判断
function isXXX(val) {
return toString.call(val) === '[object XXX]';
}

  其中一些我个人觉得可以学习一下的:

isObject

1
2
3
4
5
6
7
8
9
/**
* Determine if a value is an Object
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an Object, otherwise false
*/
function isObject(val) {
return val !== null && typeof val === 'object';
}

isFormData

1
2
3
4
5
6
7
8
9
/**
* Determine if a value is a FormData
*
* @param {Object} val The value to test
* @returns {boolean} True if value is an FormData, otherwise false
*/
function isFormData(val) {
return (typeof FormData !== 'undefined') && (val instanceof FormData);
}

isStandardBrowserEnv

  判断当前运行环境,WEB端依赖windowdocument,Native端核心则是在navigator.product上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Determine if we're running in a standard browser environment
*
* This allows axios to run in a web worker, and react-native.
* Both environments support XMLHttpRequest, but not fully standard globals.
*
* web workers:
* typeof window -> undefined
* typeof document -> undefined
*
* react-native:
* navigator.product -> 'ReactNative'
* nativescript
* navigator.product -> 'NativeScript' or 'NS'
*/
function isStandardBrowserEnv() {
if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' ||
navigator.product === 'NativeScript' ||
navigator.product === 'NS')) {
return false;
}
return (
typeof window !== 'undefined' &&
typeof document !== 'undefined'
);
}

forEach

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* Iterate over an Array or an Object invoking a function for each item.
*
* If `obj` is an Array callback will be called passing
* the value, index, and complete array for each item.
*
* If 'obj' is an Object callback will be called passing
* the value, key, and complete object for each property.
*
* @param {Object|Array} obj The object to iterate
* @param {Function} fn The callback to invoke for each item
*/
function forEach(obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return;
}

// Force an array if not already something iterable
if (typeof obj !== 'object') {
/*eslint no-param-reassign:0*/
obj = [obj];
}

if (isArray(obj)) {
// Iterate over array values
for (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// 由于 in 会根据原型链往上去拿继承的属性,而我们其实只关注当前对象所直接包含的属性,固此处通过该方式过滤
fn.call(null, obj[key], key, obj);
}
}
}
}

normalizeHeaderName

1
2
3
4
5
6
7
8
9
10
function normalizeHeaderName(headers, normalizedName) {
utils.forEach(headers, function processHeader(value, name) {
// 格式化头部,比较headers中的key及入参中的格式化name是否拼写相同(都转大写比较)
// 若两者内容相同,但大小写不一致,以入参传入的为准(添加新的key),删除原本headers中的key
if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {
headers[normalizedName] = value;
delete headers[name];
}
});
};

extend

  相当于把第二个参数内的内容继承到第一个参数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Extends object a by mutably adding to it the properties of object b.
*
* @param {Object} a The object to be extended
* @param {Object} b The object to copy properties from
* @param {Object} thisArg The object to bind function to
* @return {Object} The resulting value of object a
*/
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;
}

axios.js

  这个文件可以理解成一个入口,我们的逻辑都通过这个文件引入,再通过模块化导出供我们使用这个请求库。

  先看生成实例的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* Create an instance of Axios
*
* @param {Object} defaultConfig The default config for the instance
* @return {Axios} A new instance of Axios
*/
function createInstance(defaultConfig) {
var context = new Axios(defaultConfig);
var instance = bind(Axios.prototype.request, context);

// Copy axios.prototype to instance
utils.extend(instance, Axios.prototype, context);

// Copy context to instance
utils.extend(instance, context);

return instance;
}

  首先通过Axios构造方法new一个实例,其中传入默认的配置参数defaults。该配置参数又通过defaults.js导出。

  defaults.js比较关键,它对Axios的默认请求配置进行了封装,并且其中做了浏览器端和Node端的兼容:

1
2
3
4
5
6
7
8
9
10
11
function getDefaultAdapter() {
var adapter;
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');
}
return adapter;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
var defaults = {
// 获取默认情况的适配器属性
adapter: getDefaultAdapter(),

// 转化请求头配置
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 (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;
}],

// 转化返回结果,即我们拿到的请求结果已经是通过JSON.parse处理后的对象了
transformResponse: [function transformResponse(data) {
/*eslint no-param-reassign:0*/
if (typeof data === 'string') {
try {
data = JSON.parse(data);
} catch (e) { /* Ignore */ }
}
return data;
}],

/**
* A timeout in milliseconds to abort a request. If set to 0 (default) a
* timeout is not created.
*/
timeout: 0,

xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',

maxContentLength: -1,
maxBodyLength: -1,

// 是否请求成功
validateStatus: function validateStatus(status) {
return status >= 200 && status < 300;
}
};

defaults.headers = {
common: {
'Accept': 'application/json, text/plain, */*'
}
};

  再看看Axios这个“类”做了哪些事情:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Create a new instance of Axios
*
* @param {Object} instanceConfig The default config for the instance
*/
function Axios(instanceConfig) {
this.defaults = instanceConfig;
this.interceptors = {
// 初始化两个拦截器实例
request: new InterceptorManager(),
response: new InterceptorManager()
};
}

  先声明一个Axios函数,实例上有两个属性,一个是实例的配置信息this.defaults,另一个则是发起请求和接收响应的拦截器this.interceptors

InterceptorManager.js

  InterceptorManager这个拦截器又做了什么呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function InterceptorManager() {
this.handlers = [];
}

/**
* Add a new interceptor to the stack
*
* @param {Function} fulfilled The function to handle `then` for a `Promise`
* @param {Function} rejected The function to handle `reject` for a `Promise`
*
* @return {Number} An ID used to remove interceptor later
*/
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
this.handlers.push({
fulfilled: fulfilled,
rejected: rejected
});
return this.handlers.length - 1;
};

/**
* Remove an interceptor from the stack
*
* @param {Number} id The ID that was returned by `use`
*/
InterceptorManager.prototype.eject = function eject(id) {
if (this.handlers[id]) {
this.handlers[id] = null;
}
};

/**
* Iterate over all the registered interceptors
*
* This method is particularly useful for skipping over any
* interceptors that may have become `null` calling `eject`.
*
* @param {Function} fn The function to call for each interceptor
*/
InterceptorManager.prototype.forEach = function forEach(fn) {
utils.forEach(this.handlers, function forEachHandler(h) {
if (h !== null) {
fn(h);
}
});
};

  实例中有一个handlers初始化空数组进行拦截内容添加,原型链上添加useejectforEach方法,分别进行Promise处理状态入栈,清除以及迭代函数调用。

  然后在Axios的原型链上配置方法requestgetUri

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* Dispatch a request
*
* @param {Object} config The config specific for this request (merged with this.defaults)
*/
Axios.prototype.request = function request(config) {
/*eslint no-param-reassign:0*/
// Allow for axios('example/url'[, config]) a la fetch API
// 支持2种 request方式 当第一个参数为字符串时,即将其设置为接口URL,第二个参数为config的配置信息
// 当传入非字符串时,则将其设置为config
if (typeof config === 'string') {
config = arguments[1] || {};
config.url = arguments[0];
} else {
config = config || {};
}

// 此处将会拿我们的配置信息 与 前面代码中的defaultConfig合并 相同key的value被覆盖
config = mergeConfig(this.defaults, config);

// Set config.method
// 依次判断 传入config内的请求类型 > 默认的defaultConfig内的请求类型 , 若都不存在 则初始化为get类型请求
if (config.method) {
config.method = config.method.toLowerCase();
} else if (this.defaults.method) {
config.method = this.defaults.method.toLowerCase();
} else {
config.method = 'get';
}

// Hook up interceptors middleware
var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
chain.push(interceptor.fulfilled, interceptor.rejected);
});

while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}

return promise;
};

  实际上核心还是request函数,它在内部通过chain数组结构编织了一个请求管道,默认没有配置拦截器this.interceptors.requestthis.interceptors.response时,初始化值为[dispatchRequest, undefined]。这等价于请求时,在promise的回调中直接触发resolve状态的dispatchRequest,入参即请求配置config

  那么当我们分别在requestresponse中添加拦截,塞入中间件,就是下面这样的编排结构:

  结合请求前的处理,我们可以知道从队列首部到dispatchRequest,是给我们中间处理config的,因为dispatchRequest最终接收参数就是一个config。常见应用场景如获取app token,确认当前token是否有效等。而undefined之后到队尾的配置就是处理dispatchRequestpromise返回的response的内容,如果有相关场景依赖,我们便可以在返回的response上构造,处理起来也是生成新的Promise返回,数据返回新的response

  综上,chain通过管道的概念,形成了一个promise链式调用,队列首到dispatchRequest进行config中间处理,undefined到队列尾部进行请求返回的response的中间处理。