《源码也是js》大话axios源码

195 阅读13分钟

前言

转前端开发也有些年头了。当年的菜鸡中的战斗机,虽然还没成为大佬。不过菜的也没那么过分了。

平时经常到掘金,知乎(划水偏多),微信公众号上看技术文章。一直读着别人的输出,有时候也想自己写点啥,最好还能让别人读了之后有所收获。

就在前不久,在b站搜到解析axios的教学视频,看着看着发现:“咦,这源码感觉能读懂啊”。于是产生了写一篇学习笔记的想法。为“源码也是js”系列开个头。(如果没有更新,那这就是一个普通的标题。)其实axios的源码不是很难,但把自己理解的东西,清晰的表达出来就不容易了。尤其对程序员来说,当面沟通也可能是在“跨服聊天”。听技术分享的时候,我总是会想起中学数学老师。那平缓的语速,稳定的情绪。

我认为无论是分享还是写作,要把一部分重心放在吸引读者(听众)的注意力,引起他们的兴趣上。比如尝试加入一些比喻或故事,把生硬的知识,讲得生动。张鑫旭大佬的文章就很有趣,就是有些比喻过于抽象了 :)。

如果时间充裕,可以先看看上面链接里的视频,再结合本文,尝试读一读源码

1. axios的实例化

我们知道axios引入之后有两种用法,一是函数调用:

axios({
  method: 'post',
  url: '/user',
  data: {
    firstName: 'snow',
    lastName: 'peak'
  }
});

另一种就是当对象用,调用上面的方法:

axios.post('/user', {
  firstName: 'snow',
  lastName: 'peak'
});

是不是感觉有些熟悉,有没有想起我们的老朋友JQuery?既然可以加上括号调用,那axios毫无疑问是一个函数。在js中函数又是特殊的对象,所以可以像给正常对象添加属性一样给函数添加属性。

刚入行的头两年,曾拿着不知道从哪里看来的“万物皆是对象”这句话到处装逼。甚至有几次在面试的时候,回答完问题后如果上下文不突兀的话,也会深沉的来一句。直到后来读了“你不知道的JavaScript”,里面明确指出这句话是错的。才深深感到,人还是要多读书啊。不赚认知范围以外的钱,不装一知半解的逼。

上图是axios实例化的一个简略流程,可以看到,最终导出的是Axios构造函数的Prototype对象上的request方法。所以才可以调用它,也可以调用它上面的方法。

对照源码

这里的bind, utils.extend 是axios包里的工具函数。bind生成了一个执行上下文绑到了Axios实例上的新函数。(所以严格来说导出的不是request函数,而是绑定了上下文的新函数。)

第一次extend把prototype对象上的所有属性,复制到了这个新函数上。第二个extend把Axios实例上的所有属性复制到了这个新函数上。这一顿操作之后,我们可以把这个新函数看作axios的实例了。

接着又添加了各种属性。

红框内的东西是跟取消请求相关的属性,先留个印象,在第五节讲到取消请求的时候,再回来瞅一眼。

最后导出

2. 默认配置对象

在创建Axios实例的时候会传一个配置对象,这个对象被作者提取成了一个文件,让我们看看这里面有什么名堂。

在这之前,我们先复习一下浏览器请求相关的知识。当我们在地址栏输入一个url并回车的时候(跳过所有步骤,经典面试题就不展开了),浏览器会发送一个一般请求,接着会获取到一个html文件。解析html文件的时候又会发送无数个一般请求去请求各种资源。比如:脚本,图片,字体,css文件等等。

它们的type如下图,type为document即表示返回了一个html文件。

而ajax请求的type有两种。  type为xhr就是用经典的XHR对象发的请求,type为fetch就代表是用window上的fetch发的请求。

axios的内部使用xhr对象发送请求。这里附上阮一峰老师的fetch教程

ok, 我们继续。这里说明一下,因为篇幅的原因,本文只对一些关键代码截图说明。为了保证axios的健壮性,还有很多“非关键”代码,有兴趣的朋友,可以自己看源码。

defaults对象里最重要的就是adapter属性了。适配器,适配啥? 我们看一下上面getDefaultAdapter函数的返回。什么情况下XHR对象会是undefined?没错,机智如你应该已经猜到了。axios在浏览器环境用做发送ajax请求,在node环境用做发送http请求。我们先把node环境扔到一边,只看浏览器环境。

adapter是个函数,最核心的创建XHR对象、发送请求、都是在这里做的。我们再来看transformRequest和transformResponse,它们都是数组,并且都有一个函数成员。这里多说一句,axios的作者在可以写成匿名函数的地方,几乎都加上了函数名称。使得像我一样的英语渣渣,在看了函数体的内容,结合有道词典翻译函数名后,也能大致猜出函数的用途。

transform, 嗯, 变形、转换。 transformRequest,转换请求。 transformResponse,转换响应。嗯,very well.

简单来说就是发送请求前,对请求头和请求体的处理。

这里是获取响应后,对响应体的处理。这两个属性等用到时再好好白和白和。

3. Axios构造函数

看完了defaultConfig对象,我们再来瞧瞧Axios构造函数。

axios的特点之一,拦截器要在这里登场了。拦截器可以理解为钩子函数,请求拦截器是在请求发送之前允许用户对请求头、请求体进行操作,响应拦截器是在浏览器获取到响应之后,用户处理返回的数据之前,对响应体进行操作。

接着我们看看request里面有啥蹊跷。

红框圈出来的部分,是axios第一处有点妙的地方。chain可以理解为一个队列,拦截器就是依靠这个队列实现的。而且里面的成员数量肯定是偶数,为啥是偶数,先卖个关子。接着创建了一个立即完成的promise(就默认大家对promise比较熟了),决议值config就是把默认defaultConfig对象和用户传入的自定义的config对象(把axios当作函数调用时传入的对象),合并后的最终config对象。

然后调用了请求拦截器和响应拦截器的forEach方法,等等,new InterceptorManager不是应该返回实例对象吗,难道它是数组,不然它上面为什么会有forEach方法呢?是我们的基础不牢固吗?

喔喔 easy man~, 实例还是对象,原型上有一个自定义的forEach方法罢了。简单看一眼。

接着出现了一个while循环,条件是chain的长度。我们先忽略拦截器,就当chain里只有两个成员,dispatchRequest和undefined。第一次循环,shift了两次,作为then的第一、第二参数,也就是变成了这样。

promise.then(dispatchRequest,undefined)

到这里就能发现,request函数里真正做事情的是dispatchRequest。那个立即完成的promise的决议值config,会传到dispatchRequest里。而最核心的adapter就在config里。所以我们不妨大胆猜测一下,dispatchRequest里会调用adapter。

而undefined相当于一个占位符,promise被拒绝时默认没有任何处理。chain经过两次shift后,已经被掏空了。循环终止,return出promise,剩下的由使用者自己看着办。

接下来是使用拦截器的情况。前面我们说到chain的长度只能是偶数。从while的循环体能看出,每次从chain里shift出两个元素作为then的第一、第二参数。所以不管是请求拦截器还是响应拦截器,处理已完成的函数和被拒绝的函数都是成对出现的。请求拦截器就往chain的前面塞,响应拦截器就在chain的后面塞。每次while循环,就从chain的前面取出两个元素传给promise.then(),直到chain里面没有元素。

拦截器的具体实现,想了一下,还是不展开了。就像在故事里,无法也没必要把每个配角的身世来历交代清楚。ok,我们继续专注于主线。

4. dispatchRequest和xhrAdapter

那dispatchRequest里又有什么门道?

果不其然,里面调用了adapter,我用微信截图简单画了一下三者的关系。俄罗斯套娃的既视感有木有? 好的分层设计,使代码逻辑清晰,更加易读,提高了项目的可维护性。甚至会给人一种错落有致的结构美感。

还记得“默认配置对象”一节,defaults对象里的transformRequest属性吗,它是一个数组,里面有一个函数成员,接收data和headers两个参数。现在终于要派上用场了。

transformData是个工具函数,可以看到内部遍历了transformRequest。并且把每一轮循环时,fn的调用结果暂存到data变量上,继续传给下一轮循环时的fn里,最终返回data。

transformResponse在adapter后面的then里面也用到了。

这么看来抽出dispatchRequest这一层的原因,是为了在chain队列之外,封装重要程度更高的默认请求拦截器和默认响应拦截器。

伙计们,我们这只钻探队终于到了axios的核心部分,实际发请求的地方。让我们看看xhrAdapter内部有什么玄机。

可以看到,xhrAdapter不出意外的返回了一个promise.

也看到了亲切的身影,XHR四部曲:创建XHR对象,open,监听状态变化和send.  还有一些常用属性:设置超时时间,监听请求中止,监听异常,监听请求超时。

5. 中止请求

说到axios,就不能不谈它的中止请求功能。这也是另一处有点妙的地方。下面两段代码是从axios的github复制过来的,中止请求的用法。

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

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

两种方式原理是一样的,第一种要更优雅一些。

第一步,从axios上取出CancelToken。它是在实例化的时候加上去的,想不起来可以回到第一节回顾一下。那这是个啥玩意?从首字母大写的规范命名来看,我们能合理的推测出这是个类。(也就是构造函数,但我比较认同《你不知道的javaScript》中的说法,即没有构造函数这种东西,只有函数的构造调用。)

我们沿着第一种使用方法走。第二步调用了静态方法source. 

从这里开始要稍稍集中精神了,在source里面,创建CancelToken实例的时候传入了一个函数,这个函数接收一个参数,把它赋给外面的cancel变量。最后返回一个含有token,cancel两个属性的对象。

第三步,我们调用这个cancel,就能实现中止请求。那这个cacel函数为何有这能耐?

  

这块儿还是有点小妙的,上图中的cancel函数就是我们在第三步中调用的cancel函数。我们调用cancel的时候,在CancelToken的实例上加了一个reason属性。(可以理解为一个标记。)此处注意,接着把CancelToken的实例上的promise属性指向的promise完成掉了,这是关键一步。

我们再回到xhrAdapter文件,这次只看一个if分支。

config上怎么会有cancelToken? 哦,是我们传进去的。不信你看本节开始时的两段示例代码。

我们捋一下,在第三步调用cancel后,完成了一个promise,而在xhrAdapter里有人在等这个promise完成,好把请求中止掉,并把xhrAdapter返回的promise状态变更为被拒绝。

到这里就成功的把一个请求中止掉了。当然有一些细节,需要你自己去探索。

附录

Cancel构造函数

在上一节,我们在调用cancel的时候会传一段说明文字。

source.cancel('Operation canceled by the user.');

这段文字最终传给了Cancel构造函数。

Cancel构造函数的内部比较简单。

axios的其他用法

在第一节,我们提到axios还有这种用法。

axios.post('/user', {
  firstName: 'snow',
  lastName: 'peak'
});

你可能会觉得,怎么回事,这么重要的都不讲。这是一个误会,让我们看看axios.get, axios.post 等是怎么实现的。

作者把请求分成了两类,需要携带请求体的为一类:post, put, patch.  不需要的为一类:delete, get, head, options.  这更多是语义上的规定,实际上get请求也可以携带请求体,post请求也可以在url上拼接query字符串。我们公司就没这多讲究,请求一律post.  (注:浏览器自带的fetch方法,请求方式是get时,第二个参数里存在body属性就会报错。)

经过这两次遍历后,Axios的原型对象上,多了与七种请求方式同名的方法。而这些方法最后调用的也是request.

手动创建axios实例

axios上有一个create方法,用来创建axios的实例。一般使用默认创建好的实例,也就是导入的axios直接用,不过也会遇到一些场景,需要我们创建axios的实例。

// Set config defaults when creating the instance
const instance = axios.create({
  baseURL: 'https://api.example.com'
});

// Alter defaults after instance has been created
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;

这个方法里面调用了createInstance,是不是有点眼熟。我们回到第一节,看一下第二张配图。

后记

这篇笔记到这里就进入尾声了。 屏幕前的你,如果感到有些许收获,那我也算是达到了在文章开头提到的目的。读源码就像看美剧一样,突然出现一个新人物,或突然切到一个新的叙事线上时,一开始可能会有点懵,不要紧,暂且先看下去,到最后自然能把所有的关系用网状图连接起来。

把原来看起来像魔法一样神奇的事情,弄清原理。这或许就是阅读源码的乐趣吧。