由于axios源码中有很多不是很重要的方法,而且很多方法为了考虑兼容性,并没有考虑到用es6 的语法去写。本篇主要是带你去梳理axios的主要流程,并用es6重写简易版axios

  • 拦截器
  • 适配器
  • 取消请求

    拦截器

    一个axios实例上有两个拦截器,一个是请求拦截器, 然后响应拦截器。我们下看下官网的用法:添加拦截器

    1. // 添加请求拦截器
    2. axios.interceptors.request.use(function (config) {
    3. // 在发送请求之前做些什么
    4. return config;
    5. }, function (error) {
    6. // 对请求错误做些什么
    7. return Promise.reject(error);
    8. });

    移除拦截器

    1. const myInterceptor = axios.interceptors.request.use(function () {/*...*/});
    2. axios.interceptors.request.eject(myInterceptor);

    其实源码中就是,所有拦截器的执行 所以说肯定有一个forEach方法。
    思路理清楚了,现在我们就开始去写吧。代码我就直接发出来,然后我在下面注解。

    1. export class InterceptorManager {
    2. constructor() {
    3. // 存放所有拦截器的栈
    4. this.handlers = []
    5. }
    6. use(fulfilled, rejected) {
    7. this.handlers.push({
    8. fulfilled,
    9. rejected,
    10. })
    11. //返回id 便于取消
    12. return this.handlers.length - 1
    13. }
    14. // 取消一个拦截器
    15. eject(id) {
    16. if (this.handlers[id]) {
    17. this.handlers[id] = null
    18. }
    19. }
    20. // 执行栈中所有的hanlder
    21. forEach(fn) {
    22. this.handlers.forEach((item) => {
    23. // 这里为了过滤已经被取消的拦截器,因为已经取消的拦截器被置null
    24. if (item) {
    25. fn(item)
    26. }
    27. })
    28. }
    29. }

    拦截器这个类我们已经初步实现了,现在我们去实现axios 这个类,还是先看下官方文档,先看用法,再去分析。
    axios(config)

    1. // 发送 POST 请求
    2. axios({
    3. method: 'post',
    4. url: '/user/12345',
    5. data: {
    6. firstName: 'Fred',
    7. lastName: 'Flintstone'
    8. }
    9. });

    axios(url[, config])

    1. // 发送 GET 请求(默认的方法)
    2. axios('/user/12345');

    Axios 这个类最核心的方法其实还是 request 这个方法。我们先看下实现吧

    1. class Axios {
    2. constructor(config) {
    3. this.defaults = config
    4. this.interceptors = {
    5. request: new InterceptorManager(),
    6. response: new InterceptorManager(),
    7. }
    8. }
    9. // 发送一个请求
    10. request(config) {
    11. // 这里呢其实就是去处理了 axios(url[,config])
    12. if (typeof config == 'string') {
    13. config = arguments[1] || {}
    14. config.url = arguments[0]
    15. } else {
    16. config = config || {}
    17. }
    18. // 默认get请求,并且都转成小写
    19. if (config.method) {
    20. config.method = config.method.toLowerCase()
    21. } else {
    22. config.method = 'get'
    23. }
    24. // dispatchRequest 就是发送ajax请求
    25. const chain = [dispatchRequest, undefined]
    26. // 发生请求之前加入拦截的 fulfille 和reject 函数
    27. this.interceptors.request.forEach((item) => {
    28. chain.unshift(item.fulfilled, item.rejected)
    29. })
    30. // 在请求之后增加 fulfilled 和reject 函数
    31. this.interceptors.response.forEach((item) => {
    32. chain.push(item.fulfilled, item.rejected)
    33. })
    34. // 利用promise的链式调用,将参数一层一层传下去
    35. let promise = Promise.resolve(config)
    36. //然后我去遍历 chain
    37. while (chain.length) {
    38. // 这里不断出栈 直到结束为止
    39. promise = promise.then(chain.shift(), chain.shift())
    40. }
    41. return promise
    42. }
    43. }

    这里其实就是体现了axios设计的巧妙, 维护一个栈结构 + promise 的链式调用 实现了 拦截器的功能, 可能有的小伙伴到这里还是不是很能理解,我还是给大家画一个草图去模拟下这个过程。

假设我有1个请求拦截器handler和1个响应拦截器handler
一开始我们栈中的数据就两个axios 源码分析 - 图1
这个没什么问题,由于有拦截器的存在,如果存在的话,那么我们就要往这个栈中加数据,请求拦截器顾名思义要在请求之前所以是unshift。加完请求拦截器我们的栈变成了这样axios 源码分析 - 图2
没什么问题,然后请求结束后,我们又想对请求之后的数据做处理,所以响应拦截的数据自然是push了。这时候栈结构变成了这样:axios 源码分析 - 图3
然后遍历整个栈结构,每次出栈都是一对出栈, 因为promise 的then 就是 一个成功,一个失败嘛。遍历结束后,返回经过所有处理的promise,然后你就可以拿到最终的值了。

adapter

Adapter: 英文解释是适配器的意思。这里我就不实现了,我带大家看一下源码。adapter 做了一件事非常简单,就是根据不同的环境 使用不同的请求。如果用户自定义了adapter,就用config.adapter。否则就是默认是default.adpter.

  1. var adapter = config.adapter || defaults.adapter;
  2. return adapter(config).then() ...

继续往下看deafults.adapter做了什么事情:

  1. function getDefaultAdapter() {
  2. var adapter;
  3. if (typeof XMLHttpRequest !== 'undefined') {
  4. // For browsers use XHR adapter
  5. adapter = require('./adapters/xhr');
  6. } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
  7. // For node use HTTP adapter
  8. adapter = require('./adapters/http');
  9. }
  10. return adapter;
  11. }

其实就是做个选择:如果是浏览器环境:就是用xhr 否则就是node 环境。判断process是否存在。从写代码的角度来说,axios源码的这里的设计可扩展性非常好。有点像设计模式中的适配器模式, 因为浏览器端和node 端 发送请求其实并不一样, 但是我们不重要,我们不去管他的内部实现,用promise包一层做到对外统一。所以 我们用axios 自定义adapter 器的时候, 一定是返回一个promise。ok请求的方法我在下面模拟写出。

cancleToken

我首先问大家一个问题,取消请求原生浏览器是怎么做到的?有一个abort 方法。可以取消请求。那么axios源码肯定也是运用了这一点去取消请求。现在浏览器其实也支持fetch请求, fetch可以取消请求?很多同学说是不可以的,其实不是?fetch 结合 abortController 可以实现取消fetch请求。我们看下例子:

  1. export function dispatchRequest(config) {
  2. return new Promise((resolve, reject) => {
  3. const xhr = new XMLHttpRequest()
  4. xhr.open(config.method, config.url)
  5. xhr.onreadystatechange = function () {
  6. if (xhr.status >= 200 && xhr.status <= 300 && xhr.readyState === 4) {
  7. resolve(xhr.responseText)
  8. } else {
  9. reject('失败了')
  10. }
  11. }
  12. if (config.cancelToken) {
  13. // Handle cancellation
  14. config.cancelToken.promise.then(function onCanceled(cancel) {
  15. if (!xhr) {
  16. return
  17. }
  18. xhr.abort()
  19. reject(cancel)
  20. // Clean up request
  21. xhr = null
  22. })
  23. }
  24. xhr.send()
  25. })
  26. }

Axios 源码里面做了很多处理, 这里我只做了get处理,我主要的目的就是为了axios是如何取消请求的。先看下官方用法:
主要是两种用法:
使用 cancel token 取消请求

  1. const CancelToken = axios.CancelToken;
  2. const source = CancelToken.source();
  3. axios.get('/user/12345', {
  4. cancelToken: source.token
  5. }).catch(function(thrown) {
  6. if (axios.isCancel(thrown)) {
  7. console.log('Request canceled', thrown.message);
  8. } else {
  9. // 处理错误
  10. }
  11. });
  12. axios.post('/user/12345', {
  13. name: 'new name'
  14. }, {
  15. cancelToken: source.token
  16. })
  17. // 取消请求(message 参数是可选的)
  18. source.cancel('Operation canceled by the user.');

还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token

  1. const CancelToken = axios.CancelToken;
  2. let cancel;
  3. axios.get('/user/12345', {
  4. cancelToken: new CancelToken(function executor(c) {
  5. // executor 函数接收一个 cancel 函数作为参数
  6. cancel = c;
  7. })
  8. });
  9. // cancel the request
  10. cancel();

看了官方用法 和结合axios源码:我给出以下实现:

  1. export class cancelToken {
  2. constructor(exactor) {
  3. if (typeof executor !== 'function') {
  4. throw new TypeError('executor must be a function.')
  5. }
  6. // 这里其实将promise的控制权 交给 cancel 函数
  7. // 同时做了防止多次重复cancel 之前 Redux 还有React 源码中也有类似的案列
  8. const resolvePromise;
  9. this.promise = new Promise(resolve => {
  10. resolvePromise = resolve;
  11. })
  12. this.reason = undefined;
  13. const cancel = (message) => {
  14. if(this.reason) {
  15. return;
  16. }
  17. this.reason = 'cancel' + message;
  18. resolvePromise(this.reason);
  19. }
  20. exactor(cancel)
  21. }
  22. throwIfRequested() {
  23. if(this.reason) {
  24. throw this.reason
  25. }
  26. }
  27. // source 其实本质上是一个语法糖 里面做了封装
  28. static source() {
  29. const cancel;
  30. const token = new cancelToken(function executor(c) {
  31. cancel = c;
  32. });
  33. return {
  34. token: token,
  35. cancel: cancel
  36. };
  37. }
  38. }

截止到这里大体axios 大体功能已经给出。
接下来我就测试下我的手写axios 有没有什么问题?

  1. <script type="module" >
  2. import Axios from './axios.js';
  3. const config = { url:'http://101.132.113.6:3030/api/mock' }
  4. const axios = new Axios();
  5. axios.request(config).then(res => {
  6. console.log(res,'0000')
  7. }).catch(err => {
  8. console.log(err)
  9. })
  10. </script>

打开浏览器看一下结果:axios 源码分析 - 图4
成功了ok, 然后我来测试一下拦截器的功能:代码更新成下面这样:

  1. import Axios from './axios.js';
  2. const config = { url:'http://101.132.113.6:3030/api/mock' }
  3. const axios = new Axios();
  4. // 在axios 实例上挂载属性
  5. const err = () => {}
  6. axios.interceptors.request.use((config)=> {
  7. console.log('我是请求拦截器1')
  8. config.id = 1;
  9. return config
  10. },err )
  11. axios.interceptors.request.use((config)=> {
  12. config.id = 2
  13. console.log('我是请求拦截器2')
  14. return config
  15. },err)
  16. axios.interceptors.response.use((data)=> {
  17. console.log('我是响应拦截器1',data )
  18. data += 1;
  19. return data;
  20. },err)
  21. axios.interceptors.response.use((data)=> {
  22. console.log('我是响应拦截器2',data )
  23. return data
  24. },err)
  25. axios.request(config).then(res => {
  26. // console.log(res,'0000')
  27. // return res;
  28. }).catch(err => {
  29. console.log(err)
  30. }) console.log(err)})

ajax 请求的结果 我是resolve(1) ,所以我们看下输出路径:axios 源码分析 - 图5
没什么问题, 响应后的数据我加了1。
接下来我来是取消请求的两种方式

  1. // 第一种方式
  2. let cancelFun = undefined;
  3. const cancelInstance = new cancelToken((c)=>{
  4. cancelFun = c;
  5. });
  6. config.cancelToken = cancelInstance;
  7. // 50 ms 就取消请求
  8. setTimeout(()=>{
  9. cancelFun('取消成功')
  10. },50)
  11. 第二种方式:
  12. const { token, cancel } = cancelToken.source();
  13. config.cancelToken = token;
  14. setTimeout(()=>{
  15. cancel()
  16. },50)


axios 源码分析 - 图6
结果都是OK的,至此axios简单源码终于搞定了。

总结:

本篇文章只是把axios源码的大体流程走了一遍, axios源码内部还是做了很多兼容比如:配置优先级:他有一个mergeConfig 方法, 还有数据转换器。不过这些不影响我们对axios源码的整体梳理, 源码中其实有一个createInstance,至于为什么有?我觉得就是为了可扩展性更好, 将来有啥新功能,直接在原有axios的实例的原型链上去增加,代码可维护性强, axios.all spread 都是实例new出来再去挂的,不过都很简单,没啥的。有兴趣大家自行阅读。

https://juejin.cn/post/7008076609701806117#heading-6