axios 简介
Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js。懂的都懂!
本文将从接下来的几个点,分析 axios:
- 创建实例
- 请求发送
- 请求取消
创建实例
在axios.js文件中会初始调用一次createInstance函数。在此函数中会new Axios创建一个名为context的实例和使用bind函数对Axios.prototype.request进行包裹并返回名为instance的函数。
然后,将Axios.prototype和context的属性和方法extend到instance函数。最后,在instance上添加create方法。此方法是一个工厂方法,能够创建新的Axios实例。
源码 lib/axios.js
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);
// Factory for creating new instances
instance.create = function create(instanceConfig) {
return createInstance(mergeConfig(defaultConfig, instanceConfig));
};
return instance;
}
// Create the default instance to be exported
var axios = createInstance(defaults);
通过上述步骤后,axios 变量可以通过axios({})发送请求,也可以通过axios.get/post等方式发起请求(get/post方法都是Axios.prototype上的方法)。
请求发送
我们调用的axiox.get/post等方法,其实都是去调用的Axios.prototype.request方法。在此方法中会合并配置、执行请求拦截器和响应拦截器等。
合并配置
源码 lib/core/Axios.js
Axios.prototype.request = function request(configOrUrl, config) {
// 此处支持两种 api 的调用方式,axios('example/url'[, config]) 和 axios(config)
if (typeof configOrUrl === 'string') {
config = config || {};
config.url = configOrUrl;
} else {
config = configOrUrl || {};
}
// 合并创建实例时传入的默认配置和目前传入的配置
config = mergeConfig(this.defaults, config);
...
}
注意
通常我们在使用 axios 时会通过axios.defaults[name] = value来设置一些默认配置,作用于每一个请求。但是必须要在通过 axios.create 工厂方法创建实例之前设置。
// 正确用法
axios.defaults['timeout'] = 3000;
console.log('axios.defaults', JSON.parse(JSON.stringify(axios.defaults)));
const instance = axios.create({});
console.log('instance.defaults', JSON.parse(JSON.stringify(instance.defaults)));
// 错误用法
const instance = axios.create({});
console.log('instance.defaults', JSON.parse(JSON.stringify(instance.defaults)));
axios.defaults['timeout'] = 3000;
console.log('axios.defaults', JSON.parse(JSON.stringify(axios.defaults)));
请求拦截器
请求拦截器在我们日常开发中经常会用到。比如说我们在发送请求前记录发送时间(记录接口响应所花的时间),或者添加业务 token 之类的。请求拦截器目前分为两类:异步拦截器 和 同步拦截器。
源码 lib/core/Axios.js
// 收集请求拦截器
// 请求拦截器数组
var requestInterceptorChain = [];
// 请求拦截器同步还是异步执行
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});
异步拦截器
源码 lib/core/Axios.js
var promise;
// 请求拦截器异步流程
if (!synchronousRequestInterceptors) {
var chain = [dispatchRequest, undefined];
// chain:[onfulfilled, onRejected, ..., dispatchRequest, undefined]
Array.prototype.unshift.apply(chain, requestInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
通过 promise 的链式调用来实现异步请求拦截器。生成的 promise 链如下:
var promise = Promise.resolve(config);
promise
.then("请求拦截器函数2", "请求拦截器异常处理函数2")
.then("请求拦截器函数1", "请求拦截器异常处理函数1")
... // 可能有多个请求拦截器,也可能没有
.then(dispatchRequest, undefined)
.then("请求成功处理函数", "请求失败处理函数")
但是这样的方式也带了问题:
- 如果有请求拦截器的话,位于
promise链中的第一个异常处理函数,永远都不会执行。(如上面的请求拦截器异常处理函数2) - 如果主线程阻塞,
ajax请求会被延迟发送。(涉及event Loop,可以去单独了解) 示例代码
axios.interceptors.request.use((config) => {
return config;
}, (err) => {
return Promise.reject(err);
});
// 将被延迟发送
axios.get('https://httpbin.org/get');
function manualRequest() {
// 同步发送
const xhr = new XMLHttpRequest();
xhr.open('get', 'https://httpbin.org/get');
xhr.send();
}
manualRequest();
// 模拟耗时操作
let x = 100000000;
while (x > 0) {
x--;
}
从上图中可以看出,通过 axios 发出的请求会延迟。
同步拦截器
源码 lib/core/Axios.js
// 请求拦截器同步流程
var newConfig = config;
while (requestInterceptorChain.length) {
var onFulfilled = requestInterceptorChain.shift();
var onRejected = requestInterceptorChain.shift();
try {
newConfig = onFulfilled(newConfig);
} catch (error) {
onRejected(error);
break;
}
}
try {
promise = dispatchRequest(newConfig);
} catch (error) {
return Promise.reject(error);
}
return promise;
同步拦截器就比较简单了。同时也没上面异步拦截器的问题。
示例代码
axios.interceptors.request.use((config) => {
return config;
}, (err) => {
return Promise.reject(err);
// 使用同步拦截器
}, {synchronous: true});
axios.get('https://httpbin.org/get');
function manualRequest() {
const xhr = new XMLHttpRequest();
xhr.open('get', 'https://httpbin.org/get');
xhr.send();
}
manualRequest();
// 模拟耗时操作
let x = 100000000;
while (x > 0) {
x--;
}
从上图可以看出,使用同步拦截器时,请求不会延迟发送,两个请求基本上同时发出。
dispatchRequest
源码 lib/core/dispatchRequest.js
function dispatchRequest(config) {
// 确定发送请求的模块
var adapter = config.adapter || defaults.adapter;
return adapter(config).then(function onAdapterResolution(response) {
return response;
}, function onAdapterRejection(reason) {
return Promise.reject(reason);
});
};
在这个函数中通过适配器模式确定发送请求的模块,然后发出请求。
adapter
源码 lib/defaults/index.js
function getDefaultAdapter() {
var adapter;
if (typeof XMLHttpRequest !== 'undefined') {
// 浏览器环境使用 XMLHttpRequest
adapter = require('../adapters/xhr');
} else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// node 环境使用 HTTP/S
adapter = require('../adapters/http');
}
return adapter;
}
通过适配器模式,识别不同的环境,兼容多种格式。对外提供统一的接口。
响应拦截器
源码 lib/core/Axios.js
var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});
if (!synchronousRequestInterceptors) {
var chain = [dispatchRequest, undefined];
Array.prototype.unshift.apply(chain, requestInterceptorChain);
chain = chain.concat(responseInterceptorChain);
promise = Promise.resolve(config);
while (chain.length) {
promise = promise.then(chain.shift(), chain.shift());
}
return promise;
}
响应拦截器也是通过 promise 的链式调用来实现的。生成的 promise 链如下:
var promise = Promise.resolve(config);
promise
.then(dispatchRequest, undefined)
.then("响应拦截器1", "响应拦截器异常处理函数1")
.then("响应拦截器2", "响应拦截器异常处理函数2")
.then("请求成功处理函数", "请求失败处理函数")
请求取消
axios 支持两种取消请求的方式。CancelToken 和 AbortController。在 v0.22.0 后开始,不推荐使用 CancelToken。
Cancel Token
源码 lib/cancel/CancelToken.js、lib/adapters/xhr.js
// CancelToken.js
function CancelToken(executor) {
var resolvePromise;
this.promise = new Promise(function promiseExecutor(resolve) {
resolvePromise = resolve;
});
var token = this;
this.promise.then(function(cancel) {
if (!token._listeners) return;
var i = token._listeners.length;
while (i-- > 0) {
token._listeners[i](cancel);
}
token._listeners = null;
});
executor(function cancel(message) {
if (token.reason) {
// Cancellation has already been requested
return;
}
token.reason = new CanceledError(message);
resolvePromise(token.reason);
});
}
CancelToken.prototype.subscribe = function subscribe(listener) {
if (this.reason) {
listener(this.reason);
return;
}
if (this._listeners) {
this._listeners.push(listener);
} else {
this._listeners = [listener];
}
};
CancelToken.source = function source() {
var cancel;
var token = new CancelToken(function executor(c) {
cancel = c;
});
return {
token: token,
cancel: cancel
};
};
// xhr.js
if (config.cancelToken) {
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || (cancel && cancel.type) ? new CanceledError() : cancel);
request.abort();
request = null;
};
config.cancelToken && config.cancelToken.subscribe(onCanceled);
}
示例代码
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 {
// 处理错误
}
});
// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
取消请求的流程比较复杂:
- 通过
const source = CancelToken.source();生成一个 source 对象,source 对象中 token 属性是一个CancelToken实例,cancel 是取消 token 代表的 promise 实例的函数。 - 将 source 对象的 token 传递给请求的 config 中。
- 在
xhr.js中,如果判断 config.cancelToken 为真,将 onCanceled 函数订阅到 source.token 对象的_listeners数组中。 - 当调用 source.cancel() 时,将 source.token 对象内的 promise 状态改为 fulfilled,然后执行 then 函数,遍历 source.token 对象的
_listeners数组,然后执行每一个 onCanceled 函数。 - 执行 onCanceled 函数,将 XMLHttpRequest 实例对象 reject 和 abort 请求。
AbortController
源码 lib/adapters/xhr.js
if (config.cancelToken || config.signal) {
onCanceled = function(cancel) {
if (!request) {
return;
}
reject(!cancel || (cancel && cancel.type) ? new CanceledError() : cancel);
request.abort();
request = null;
};
if (config.signal) {
config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
}
}
示例代码
const controller = new AbortController();
axios.get('/foo/bar', {
signal: controller.signal
}).then(function(response) {
//...
});
// 取消请求
controller.abort()
AbortController的取消请求流程相对简单一点:
- 通过
const controller = new AbortController();生成一个 controller 对象。 - 将 controller 对象的 singal 传递给请求的 config 中。
- 在
xhr.js中,如果判断 config.singal 为真,将订阅 config.singal 的 abort 事件。 - 当执行 controller.abort() 时,触发订阅的 abort 事件。执行 onCanceled 函数,将 XMLHttpRequest 实例对象 reject 和 abort 请求。
目前只看了这么多,以后有时间再完善!