没事读读码…
axios
在业务中请求用得比较多了,这个周末就花点时间阅读下源码,先进github
开启sourcegraph
插件,找到核心实现目录:
那从哪里开始慢慢看比较好呢?我个人其实比较倾向从工具方法里入手,可以学习其中的编码思路,发现平常自己实现相同功能容易忽略的细节。了解了其中的工具方法作用后,对我们后续盘核心代码的逻辑也会轻松不少。
helpers
该文件目录下主要是一些对我们发送请求时,拼接URL、处理参数的工具方法。
bind.js
1 | ; |
其实就是在ES5下,实现了一个bind
,返回一个闭包,最后返回绑定this
的调用返回结果。
adapters
根据文件目录,及README.md,该模块主要进行浏览器端及Node端的网络请求兼容,它会处理成一个request请求进行dispatch,并且当response返回后,处理返回一个Promise。
cancel
这个模块进行了基本的Cancel对象封装,主要用于判断请求是否被Cancel以及如何进行请求的Cancel。
Cancel.js
构造了一个Cancel函数,初始化message
信息,原型链上重载了toString
方法以及添加了__CANCEL__
标记变量:
1 | ; |
CancelToken.js
CancelToken
是一个用于进行请求取消操作的对象:
1 | ; |
通过源码阅读我们可以看出,CancelToken
也是一个构造函数,它接受一个执行函数executor
,内部通过一个Promise实例控制,比较有趣的是它将Promise状态改变的回调函数执行句柄提出,并在executor
中执行后再触发。并在执行函数触发时,首次执行会调用之前Cancel
的构造函数,并将生成的实例赋值给当前上下文的reason
属性。在下一次再触发时,若已有reason
内容,则不再执行该函数。
除此之外,在axios中真正使用CancelToken
往往不是直接通过new
构造,而是使用函数的静态方法source
,它通过注入一个函数的形式,从CancelToken
内部拿到了真正进行取消动作的cancel
函数,并将其赋值给了外层source
函数内部的cancel
变量,最终返回了这个CancelToken
实例以及与其匹配的取消方法,形成一个闭包。
isCancel.js
判断任务是否已被取消,从构造上来说,它的入参是Cancel函数构造的实例,返回值的处理也比较巧妙,运用!!
真值处理,因为如果value
为undefined
,返回的就是undefined
,真值处理会进行布尔值转换。
1 | ; |
axios是怎么做请求取消的?
了解了以上构造函数实现后,我们知道了核心是CancelToken.source
方法以及CancelToken
实例原型链上对应的__CANCEL__
属性。再看看真实应用的例子from README.md:
1 | const CancelToken = axios.CancelToken; |
从使用demo上,我们知道在axios进行请求时,我们的CancelToken
实例作为参数传入,而取消句柄则在外部被我们开发者在对应业务场景消费,简单来说就是我们可以决定何时取消。
那么实际我们的axios实例是如何运用以上的token和对应的cancel呢?
1. Axios构造函数实现核心请求方法request
,本质上不同的请求方法(get
、put
、post
等)最终都会调用这个request
。
1 | // Provide aliases for supported request methods |
2. request
中间的请求体会通过interceptors
形成一个中间件进行处理,这里我们先不看具体中间件做了什么动作,聚焦到里面实际发起请求的函数dispatchRequest
,代码中的adapter
其实就是浏览器和Node端对应真实发起请求的方法封装,它们最终会返回一个Promise。
1 | // 省略部分 ... |
在这个Promise的回调中,我们会判断config
配置是否有cancelToken
属性,即是否配置了取消请求的方法,如果有,则检查其中是否已经存在了取消的reason
属性,这个reason
属性根据前文,它是一个Cancel
对象,内部是我们外部调用取消方法传入的msg
。即如果这个cancelToken
实例此时存在reason
了,它就会抛出这个内部的reason
即Cancel对象。
1 | /** |
3. adapter
我们就以浏览器端的实现xhr.js
文件来看,可以看出浏览器端就是去构造一个XMLHttpRequest
,在真实发送send
前,判断config
中是否有cancelToken
,有则以cancelToken
内部的promise
来进行异步控制。通过前文的了解我们知道,在具体业务场景我们调用cancelToken
匹配的cancel
方法进行请求终止,其实就是将CancelToken
内部的promise
进行resolve
并使得在adapter
中进入异步等待回调的promise立马回调,将XMLHttpRequest
的请求实例通过abort
方法终止。然后这个axios请求的Promise将会reject
,里面的属性就是cancelToken
的reason
。同时释放request
内存。实际上在请求的各个阶段结束后,如错误、终止、完成都会清空request
指向,这也是CancelToken
的promise
回调中发现request
已经阶段完成就啥都不做的判断逻辑:
1 | module.exports = function xhrAdapter(config) { |
utils.js
该文件下,主要是一些判断类型的工具方法:
一个基本的判断类型思路:
1 | // 获取toString 方便后续调用 避免每次都用.重新查找获取 |
其中一些我个人觉得可以学习一下的:
isObject
1 | /** |
isFormData
1 | /** |
isStandardBrowserEnv
判断当前运行环境,WEB端依赖window
及document
,Native端核心则是在navigator.product
上。
1 | /** |
forEach
1 | /** |
normalizeHeaderName
1 | function normalizeHeaderName(headers, normalizedName) { |
extend
相当于把第二个参数内的内容继承到第一个参数中。
1 | /** |
axios.js
这个文件可以理解成一个入口,我们的逻辑都通过这个文件引入,再通过模块化导出供我们使用这个请求库。
先看生成实例的方法:
1 | /** |
首先通过Axios
构造方法new
一个实例,其中传入默认的配置参数defaults
。该配置参数又通过defaults.js
导出。
defaults.js
比较关键,它对Axios的默认请求配置进行了封装,并且其中做了浏览器端和Node端的兼容:
1 | function getDefaultAdapter() { |
1 | var defaults = { |
再看看Axios
这个“类”做了哪些事情:
1 | /** |
先声明一个Axios
函数,实例上有两个属性,一个是实例的配置信息this.defaults
,另一个则是发起请求和接收响应的拦截器this.interceptors
。
InterceptorManager.js
InterceptorManager
这个拦截器又做了什么呢?
1 | function InterceptorManager() { |
实例中有一个handlers
初始化空数组进行拦截内容添加,原型链上添加use
、eject
、forEach
方法,分别进行Promise
处理状态入栈,清除以及迭代函数调用。
然后在Axios
的原型链上配置方法request
、getUri
:
1 | /** |
实际上核心还是request
函数,它在内部通过chain
数组结构编织了一个请求管道,默认没有配置拦截器this.interceptors.request
及this.interceptors.response
时,初始化值为[dispatchRequest, undefined]
。这等价于请求时,在promise
的回调中直接触发resolve
状态的dispatchRequest
,入参即请求配置config
。
那么当我们分别在request
和response
中添加拦截,塞入中间件,就是下面这样的编排结构:
结合请求前的处理,我们可以知道从队列首部到dispatchRequest
,是给我们中间处理config
的,因为dispatchRequest
最终接收参数就是一个config
。常见应用场景如获取app token,确认当前token是否有效等。而undefined
之后到队尾的配置就是处理dispatchRequest
的promise
返回的response
的内容,如果有相关场景依赖,我们便可以在返回的response
上构造,处理起来也是生成新的Promise返回,数据返回新的response
。
综上,chain
通过管道的概念,形成了一个promise
链式调用,队列首到dispatchRequest
进行config
中间处理,undefined
到队列尾部进行请求返回的response
的中间处理。