关于axios请求源码分析

什么是axios

axios是基于Promise封装的一个前端请求库,可以用在node.js和浏览器中。

axios的特点:

  • 请求返回Promise,可以很方便的进行链式调用
  • 可以附加拦截器:请求拦截器、响应拦截器
  • 可以随时取消未完成的请求
  • 客户端支持防御 XSRF(跨站请求伪造)
  • 会帮你转换请求数据和响应数据
  • 在node.js中发送请求使用http,在浏览器使用XMLHttpRequest

安装

$ npm install axios

简单使用:

get请求

axios.get('/user', {
    
    
    params: {
    
    
      ID: 12345
    }
  })
  .then(response => {
    
    
 	console.log(response);
  })
  .catch(error => {
    
    
 	console.log(error);
  });

post请求

axios.post('/user', {
    
    
    firstName: 'Fred',
    lastName: 'Flintstone'
  })
  .then(response => {
    
    
 	console.log(response);
  })
  .catch(error => {
    
    
 	console.log(error);
  });

因为是基于 promise 封装的,所以 axios 也支持 promise.all 的写法,同时 axios.spread 会 依次帮你展开请求相应结果

function getUserAccount() {
    
    
  return axios.get('/user/12345');
}

function getUserPermissions() {
    
    
  return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
  .then(axios.spread(function (acct, perms) {
    
    
    // 两个请求现在都执行完成
  }));

深入学习axios

在学完axios的用法,以及上手使用了axios之后,我心里就有些疑问:

  1. axios是怎么附加的拦截器呢?
  2. axios 的取消请求具体又是怎么实现的?
  3. axios和我们自己调用create方法产生的实例有什么区别呢?

首先要搞清axios发送请求的整个流程。我们稍微分析一下就能知道,请求拦截器肯定是在发送请求之前被调用的,而响应拦截器肯定是在请求结束,拿到返回结果的时候,再联想到官方文档介绍 axios 是基于promise 的, 那这整个流程肯定就是基于promise的链式调用了嘛。

去 github 拿到 axios 的源码 ,然后我们来捋一捋:

拿到源码的之后,我发现了两个axios文件,一个首字母大写,一个小写!我们日常写代码,不都是用的小写axios,或者创建的instance吗?

在这里插入图片描述
然后我去看了下入口文件

在这里插入图片描述

在这里插入图片描述
入口文件不是小写的 axios.js 嘛?所以这个大写的Axios是个啥?

带着这个疑问,我们先去看下axios.js,默认导出了axios,axios又是createInstance方法的返回值,而且后面一句axios.create,这不就是axios创建实例的方法嘛?同样是调用了createInstance方法,区别是传入的config配置不同。

再往后面看,发现在var 了 axios后,又往axios上加了取消请求的相关方法,和 all 等等一些方法。而我们自己再代码中调用create方法返回的实例并没有这些!这就解决了我的第三个疑问!

在这里插入图片描述

axios和我们自己调用create方法产生的实例有什么区别

看完以上代码我们可以总结下,先说相同点

  • 都能发任意请求
  • 都有默认配置和拦截器属性

不同点

  • 默认匹配的值可能不同,由于使用create方法创建的instance也支持传入配置项,这里的配置项在合并时会覆盖掉默认的axios.default
  • instance原型对象上没有取消请求相关,以及axios.all,axios.spead等方法

然后我们去看下createInstance 方法里,再给个特写

在这里插入图片描述
这里定义的context 是 new 的 大写Axios,后面用于return 的 instance调用了bind,里面传入的是Axios原型对象上的request,和大写Axios的实例,再到后面两句代码都跟Axios有关。也就是说axios从功能上来说,完全就是Axios的实例啊,然后去 Axios.js文件里看下:

在这里插入图片描述

拦截器是怎么附加的

第一段代码 定义了defaults 默认配置,以及拦截器。然后我好奇的点开 InterceptorManager 看下

在这里插入图片描述

InterceptorManager 里定义了 一个 handlers 数组,再往下看 一个 use 函数被添加到它的原型对象上。从 use 函数 的 fulfilled 和 rejected 两个参数的命名也可以猜出来, 这两个参数就是我们添加拦截器时传入的promise,看下下面添加拦截器的代码就懂了。
use 函数的作用也很明显 把这两个函数添加到 handlers 数组中,所以 axios 的拦截器是可以添加多个的。

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

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    
    
    // 对响应数据做点什么
    return response;
  }, function (error) {
    
    
    // 对响应错误做点什么
    return Promise.reject(error);
  });

最后 use 函数返回了 handlers 数组的长度减1,刚开始我是没看懂的,后来看到关于返回值的注释 An ID used to remove interceptor later 翻译过来就是 用于以后删除拦截器的ID ,也就是被加入到handlers 数组中对应的索引呐。
联想到 axios 是可以删除 拦截器的,下面 eject 函数的作用也就明显了。

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

在这里插入图片描述
至于 InterceptorManager 原型对象上的 foreach 函数,看起来像是循环 handles 数组,执行传入的 fn 函数 。这个暂且放下。

回到Axios .js文件中,下面代码往Axios原型对象上添加了一个 request 方法
在这里插入图片描述

这个request方法暂且也放下,再往下看

在这里插入图片描述
这里调用了工具类中的foreach方法, 为Axios的原型对象上添加对应的请求方法,如get,post等,我们调用的Axios.get,post等方法,就来源于这些代码。
继续往下看,Axios原型对象上的这些方法内,实际调用了this.request ,这里的this.request 就是我们上面跳过的那个 request方法,也就是说我们调用axios发起请求后,所有的操作都走了这个 request 方法,来看下下面这个request方法(英文注释都被我换成了中文)

在这里插入图片描述
request方法接收一个 config 参数,功能如其名,明显这就是个配置项。下面的第一段代码,意为 根据config 类型做相应的处理,然后合并传入的 config 与 defaults.config,这段跳过。
第二段意为 设置请求方法,默认为 get请求 ,这段跳过。
第三段重点来了,先定义一个数组,数组中有两个默认值,一个是请求分发的函数,从 dispatchRequest.js 引入,这里先跳过,第二个是 undefined ,为什么是个undefined 呢 ?
先往后看
后面定义一个变量,存储promise成功的方法,这个promise里存的就是上面合并的config,随后循环数组,嗯????…这个foreach有点不对啊,这好像不是Array对象原型上的循环方法啊, f12进去发现,这丫不是 InterceptorManager 原型对象上的方法嘛。

在这里插入图片描述
不过也确实是循环拦截器数组,调用传入的 fn函数,返回去看这个fn函数,把请求拦截器按照后来先进的顺序加入到 chain 的头部,响应拦截器按照先来先进的顺序加入到尾部。中间部分不就是之前定义数组时候,就放进去的 请求 分发嘛。

在这里插入图片描述

再往下,就是循环chain数组,每次取两个元素,刚好一个是成功的回调,一个是失败的回调,用promise链把整个流程链接起来,形如 :

(resolve0,reject0)=>(resolve1,reject1)=>(dispatchRequest, undefined)=>(resolve3,reject3)

而且贯穿整个promise请求的参数就是请求配置的config。这时候大概也就明白为什么会定义一个undefined了,因为每次都是从chain数组内取两个元素出来,而拦截器传入的都是两个回调,所以dispatchRequest,后面就需要定义一个undefined来占位置。以免位置出现偏差。而promise状态的成功失败与否都由dispatchRequest内处理。

当我们调用axios.get 或者 post 的时候,promise链步骤应该是这样

  1. 请求拦截器 (成功,或者失败的回调)
  2. 请求分发(成功的回调, undefined)
  3. 响应拦截器 (成功,或者失败的回调)
  4. 最终我们代码自己定义的成功,或者失败的回调

捋到这为止,就解开了心里的第一个疑惑:拦截器时怎么附加的

1、当调用 use 函数时,会往Axios的 interceptors 属性的 request 或者 response 数组里添加两个promise的回调函数,分别对应调用use函数时,传入的两个回调。

2、在Axios的 request 方法里,会定义一个chain数组,默认存放dispatchRequest,也就是请求分发,然后会循环request和response两个数组,把请求拦截器插入到chain数组的头部,响应拦截器插入到chain数组尾部

3、循环chain数组,每次取出两个元素,刚好是promise的成功回调和失败回调,并把这些promise 链在一起。

搞懂了拦截器是怎么附加的,但是axios还有个重点部分,发送请求,也就是之前看到的 定义chain数组时,就默认给到的dispatchRequest ,接下来去研究一下 dispatchRequest.js里究竟做了什么事情

dispatchRequest.js
前面一些代码主要是一些请求数据转换,以及请求头的处理,重点在下面

在这里插入图片描述
首先是定义adapter请求适配器,然后定义将要传入adapter内的resolve和reject函数,可以明显的看到,两者又对请求结果进行了处理。
那么 config.adapter || defaults.adapter 来自哪呢,我去们看下 default.js,开头所说的 axios的特点之一:
在node.js中发送请求使用http,在浏览器使用XMLHttpRequest便来自下面代码。

在这里插入图片描述
我们先去看浏览器环境下,使用的xhr,暂且跳过node环境的http。找到xhr.js

在这里插入图片描述
第一行,第二行代码是拿到请求头和请求体。

第三行是针对请求体内容FormData类型时,删除设置的content-type,交由浏览器自行设置。之所以要交给浏览器设置是因为请求体内容为FormData类型时,多为文件上传,而文件上传时,涉及到一个 boundary ,也就是分隔符,如果手动设置了content-type的话,就必须自己传入一个boundary ,否则上传文件就会失败。而交由浏览器自己设置的话,浏览器就会自己生成随机的boundary 。至于boundary 更详细的解释,可以自己去谷歌一下。

第四行new了一个XMLHttpRequest的实例。
第五行是关于authentication身份认证的设置,第六行 。。。。后面的操作暂且跳过

先看下监听请求状态变化的函数,
在这里插入图片描述
前面代码是些判断,以及响应头,响应体处理,然后我们看下settle方法,传入的是从dispatchRequest里传入到xhr中的resolve和reject方法,以及响应体,并根据响应状态码,确定请求成功还是失败
在这里插入图片描述
第一行代码中的validateStatus方法也在defaults.js中
在这里插入图片描述

axios 的取消请求具体又是怎么实现的

上面代码都比较简单,但是我们至今都还没找到取消请求在哪啊,我们回到xhr.js接着往下看,终于找到了cancelToken相关。
在这里插入图片描述
代码先判断是否配置了cancelToken,再回顾我们取消请求时,需要写的代码 ,

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.');

当我们传入了cancelToken,就会执行后面的代码,也就是一个promise的成功回调,在这个回调函数里调用了request.abort()方法中断请求,并且调用从dispatchRequest里传进来的reject方法,以失败结束这段promise请求。至此终于逮到了取消请求在哪写的了。

接下来去cancelToken.js中,这里首先定义了一个resolvePromise用来保存promise的resolve操作

在这里插入图片描述
在这里插入图片描述
cancelToken接收一个执行器,其实这个执行器接收的cancel函数,也就是我们取消请求时所调用的source.cancel()。
当我们在外界代码中调用source.cancel(reason)时,这个函数被调用,然后resolvePromise被调用,使这个promise成功,进入then回调,也就是xhr.js中的那段代码来中断请求。

  config.cancelToken.promise.then(function onCanceled(cancel) {
    
    
        if (!request) {
    
    
          return;
        }

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

然后再来看一点点细节,执行器的函数内部首先判断了token.reason,这是为什么呢?再往下看一步,token.reason被赋予了 Cancel的实例,而Cancel内部是这样的

在这里插入图片描述
也就是说,如果token.reason存在,那么请求就必定被取消过了,这时就执行跳出。并且注意下 最后一句
Cancel.prototype.CANCEL = true;
往Cancel的原型对象上加这个属性有什么用呢,还记得dispatchRequest.js中,关于请求失败的回调,

在这里插入图片描述
有一个判断, isCacel(),也就是Cancel.js文件内下面这句代码,看到这不得不佩服框架作者设计的巧妙

在这里插入图片描述
至此,第二个疑问,关于取消请求的原理也搞清楚了。

1、在xhr.js中,xhrAdapter 内,定义了下面一段代码,判断发送请求时,是否在config内传入了cancelToken。如果有传,则定义好cancelToken的then回调。并在回调内进行中断请求,以及结束整个promise链,进入失败回调

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

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

2、 然后在cancelToken.js内,定义了一个promise,当我们在代码中执行,source.cancel(abort reason)时,调用resolve()方法,使promise成功,进入第一步所说的then回调,从而中断请求。

补充:
至于ssource.cancel(abort reason)方法怎么来的,以为什么调用它就会使promise进入then回调,可以参考下面源码:
这是我们取消请求时的写法:

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.');

这是源码

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);
  });
}

CancelToken.source = function source() {
    
    
  var cancel;
  var token = new CancelToken(function executor(c) {
    
    
    cancel = c;
  });
  return {
    
    
    token: token,
    cancel: cancel
  };
};

猜你喜欢

转载自blog.csdn.net/qq_41885871/article/details/109205583