axios源码学习,你上你也行!

1,515 阅读14分钟

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」。

学习原因

axios 作为一个在前端领域非常重要的 ajax 请求库,不管是 VueReact还是node ,都可以使用axios进行ajax请求,可以说,axios 是前端非常重要的一块,掌握 axios 的原理、对我们日常开发和技术学习有着非常重要的意义。

Axios中文文档

学习内容

  • 了解axios发送请求和实现过程
  • 了解axios拦截器的工作原理
  • 了解axios取消请求功能
  • 模拟上三条功能

学习准备

以下准备是为了模拟功能实现做准备,不模拟可以跳过。

  1. 使用npm中的json-server包创建一个静态服务器,提供我们开发时需要的接口。
  2. 可以选中全局安装 npm install -g json-server
  3. 在创建一个db.json文件
  4. 将下面内容添加道db.json
    "posts": [
        {
            "id": 1,
            "title": "json-server",
            "author": "typicode"
        },
        {
            "id": 2,
            "title": "json-server2",
            "author": "typicode2"
        }
    ],
    "comments": [
        {
            "id": 1,
            "body": "some comment",
            "postId": 1
        }
    ],
    "profile": {
        "name": "typicode"
    }
}
  1. 使用json-server --watch db.json启动服务,可以在3000端口访问响应资源http://localhost:3000/posts

开始

  • 打开vscode终端下载axios包 npm i axios, 得到的axios主要目录为:
├── /dist/                     # 项目输出目录
├── /lib/                      # 项目源码目录
│ ├── /cancel/                 # 定义取消功能
│ ├── /core/                   # 一些核心功能
│ │ ├── Axios.js               # axios的核心主类
│ │ ├── dispatchRequest.js     # 用来调用http请求适配器方法发送请求
│ │ ├── InterceptorManager.js  # 拦截器构造函数
│ │ └── settle.js              # 根据http响应状态,改变Promise的状态
│ ├── /helpers/                # 一些辅助方法
│ ├── /adapters/               # 定义请求的适配器 xhr、http
│ │ ├── http.js                # 实现http适配器
│ │ └── xhr.js                 # 实现xhr适配器
│ ├── axios.js                 # 对外暴露接口
│ ├── defaults.js              # 默认配置 
│ └── utils.js                 # 公用工具
├── package.json               # 项目信息
├── index.d.ts                 # 配置TypeScript的声明文件
└── index.js                   # 入口文件

axios的基本使用

  • axios中文文档中我们可以看到axios有很多API, axios(config)axios.get(config)axios.post(config)axios.creat(config)...

axios的运行流程

image.png

axios的创建过程

  • axios为什么可以当函数使用,又可以当对象使用?

  • 通过index.js入口文件进入,发现主要核心是/lib/axios,在这个文件中我们可以找到答案

// axios.js
...
function createInstance(defaultConfig) {
  /* 
  创建Axios的实例
      原型对象上有一些用来发请求的方法: get()/post()/put()/delete()/request()
      自身上有2个重要属性: defaults/interceptors
  */  
  var context = new Axios(defaultConfig);

  // axios和axios.create()对应的就是request函数
  // Axios.prototype.request.bind(context)
  var instance = bind(Axios.prototype.request, context); // axios

  // 将Axios原型对象上的方法拷贝到instance上: request()/get()/post()/put()/delete()
  utils.extend(instance, Axios.prototype, context); 

  // 将Axios实例对象上的属性拷贝到instance上: defaults和interceptors属性
  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.js
// Axios构造函数
function Axios(instanceConfig) {
  // 将指定的config, 保存为defaults属性 
  this.defaults = instanceConfig;
  // 将包含请求/响应拦截器管理器的对象保存为interceptors属性
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
Axios.prototype.request = function request(config) {
 ...
};

// 用来得到带query参数的url
Axios.prototype.getUri = function getUri(config) {
  ...
};
//添加request 方式
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  ...
});
  1. axios为createInstance函数返回的对象
  2. 在createInstan函数中,定义一个Axios实例context,使用bind将Axios原型上的request方法的this绑定到context上并将它赋值给instance。使得axios可以作为函数使用(axios()...)
  3. 再将Axios原型对象上的方法拷贝到instance上,使得axios可以作为对象使用(axios.get()...)
  4. 在将Axios实例上的属性拷贝给instance上。axios可以使用拦截器(axios.interceptors.request.use()...)
  5. 最后将create方法交给instance,使其可以递归地用于扩展客户端。在连接到多个具有某些共同点但并非全部的远程设备的情况下,可以针对已经构造的实例再次封装构造,提供深度构造控制器能力

模拟axios的创建

        // 构造函数
        function Axios(config) {
            // 初始化
            this.defaults = config   //为了创建 default  默认属性
            this.intercepters = {
                request: {},
                Response: {}
            }
        }
        // 原型添加相关方法
        Axios.prototype.request = function (config) {
            console.log("发送ajax请求,请求方法为"+ config.method);
        }   
        Axios.prototype.get = function (config) {
            return Axios.prototype.request({method:"GET"})
        }
        Axios.prototype.post = function (config) {
            return Axios.prototype.request({ method: "POST" })
        }
        // 声明函数
        function createInstance(config){
            // 实例化一个对象
            let context = new  Axios(config)  // context 可以使用Axios原型上的方法, 如context.get() constext.post,但不能当做函数使用context()
            //  创建请求函数
            let instance = Axios.prototype.request.bind(context) //bind方法会创建一个新的函数 instance 已经是一个函数, 并且可以使用instance传参发送请求,但只能使用request函数, 而且不能作为对象使用,因为它上面并没有get、post、put等方法和defaults、intercepters属性
            // 将 Axios.prototype  对象中的方法添加到instance函数对象中
            Object.keys(Axios.prototype).forEach(key =>{
                instance[key] = Axios.prototype[key].bind(context)  // 使用bind确保Axios.prototype对象中的方法中this指向Axios,能够使用this.defaults、this.intercepters
            })
            // 为instance 函数对象添加属性  default、 intercepters
            Object.keys(context).forEach(key => {
                instance[key] = context [key]
            })
            // 返回 instance  此时的instance既可以当函数使用,也可以调用方法
            return instance
        }
        //测试
        let axios = createInstance()
        // 发送请求
        axios({method:"GEt"})
        axios({method:"POST"})
        axios.get({})
        axios.post({})
  • 这里并没有实现request,也没有实现xhr请求,下面会一一模拟

axios请求过程

  • 通过上面axios创建过程我们知道axios()实际是Axios.prototype.request方法,那么request方法怎么实现请求的呢?

  • Axios.js中现在的Axios.prototype.request在进行修改过,作者为了避免拦截器可能会出现异步情况或有很长的宏任务执行,并且重构之前的代码中,因为请求事放到微任务中执行的,微任务创建的时机在构建promise链之前,如果当执行到请求之前宏任务耗时比较久,或者某个请求拦截器有做异步,会导致真正的ajax请求发送时机会有一定的延迟,所以解决这个问题是很有必要的。

  • 为了更好的理解pr后的改变,和改变的原因,我们先了解一下修改前的。

  • 之前的Axios.prototype.request

Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // 合并配置  --mergeConfig.js
  config = mergeConfig(this.defaults, config);
  // 添加method配置, 默认为get
  config.method = config.method ? config.method.toLowerCase() : 'get';
  //创建用于保存请求/响应拦截函数的数组
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 后添加的请求拦截器保存在数组的前面
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // 后添加的响应拦截器保存在数组的后面
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  
  // 通过promise的then()串连起所有的请求拦截器/请求方法/响应拦截器
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  // 返回用来指定我们的onResolved和onRejected的promise
  return promise;
};
  • 整体流程: request(config) ==> dispatchRequest(config) ==> xhrAdapter(config)
  1. config判断和初始化,介于axios的调用方式不同。再通过mergeConfig函数对传入的configthis.default比较,合并配置对象中指定属性名的属性, 优先使用config中的配置。判断请求的类型方式,有就小写化,没有就默认为get
  2. 创建用于保存请求/响应拦截函数的数组chain,数组的中间放发送请求的函数,数组的左边放请求拦截器函数(成功/失败),数组的右边放响应拦截器函数。

image.png

  • 为什么chain数组需要两个元素,又为什么是undefined?
  1. 如上图,我们都知道axios是基于promise的http库,Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。chain数组两个元素就表示fulfilledrejected,成功就调用dispatchRequest、失败执行undefined
  2. 这里的undefined没有什么具体意义,是一个占位符,当请求拦截器失败后会执行到这里的undefined,promise具有异常穿透,当使用 promise 的 then 链式调用时, 可以在最后指定失败的回调,前面任何操作出了异常, 都会传到最后失败的回调中处理。所以chain中的undefined会跳转到响应拦截器的失败回调。
  • 具体实现会在下面拦截器说明
  1. 发送请求的函数为dispatchRequest,发送流程dispatchRequest.js->default.js中getDefaultAdapter()->adapters.js中xhr.js或http.jsxhr.js使用是传统的XMLHttpRequest对象发起请求,http.js使用的是node模块http.
  2. 然后通过promise的then()串连起所有的请求拦截器/请求方法/响应拦截器,返回最后的promise。

pr后的Axios.prototype.request

读完个人认为: 原因是在执行下面这行代码的时候,通过前面的拦截器(成功)后,当执行dispatchRequser函数进行xhr请求的时候,在请求没有转为promise之前,while循环还在向前运行,会占用主线程,导致axios请求延迟发送,响应页面延迟呈现,影响跳出率/转化率和用户体验。并不是每个人都利用拦截器,并且仅在使用它们时创建承诺可能是解决此问题的好方法,

  看pr代码:将while循环拆开了,响应拦截没有变,将请求拦截进行判断,因为用户提供的自定义适配器函数不能保证返回一个 Promise,如果用户设置的拦截器函数是异步的,就会通过interceptor.synchronous参数设置synchronousRequestInterceptors为false,进行没pr前的拦截器链式调用。如果用户设置的拦截器函数是同步的,则synchronousRequestInterceptors默认为true(即默认是同步),没有修改,然后将dispatchRequest函数从while循环中抽出,先执行拦截器,等待所有拦截器完成后调用dispatchRequest进行xhr请求,等待请求结束后在进行响应拦截器运行。

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

image.png

模拟请求

由于修改后的Axios.prototype.request复杂和难度较高(写不来),但其核心链式调用还是不变的,所有我只能模拟修改前的请求。偷笑.jpg

function Axios(config){
        this.config = config
    }
    Axios.prototype.request = function(config){
        // 发送请求
        //  ...完善 合并config  
        // 创建一个promise 对象
        let promise = Promise.resolve(config)
        // 声明一个数组
        let chains = [dispatchRequest, undefined]  // underfined  占位
        //  调用then方法指定回调
        let result = promise.then(chains[0], chains[1])
        return result
    }
    // 2. dispatchRequest
    function dispatchRequest(config){
        // ...一系列操作
        // 调用适配器发送请求
        return xhrAdapter(config).then(response =>{
            // ... 对响应的结果进行转换处理
            return response
        },error =>{
            throw error
        })
    }
    // 3. adapter 适配器
    function xhrAdapter(config){
        console.log("xhrAdapter 函数执行");
        return new Promise((resolve, reject)=>{
            // 发送ajax请求
            let xhr = new XMLHttpRequest()
            // 初始化
            xhr.open(config.method, config.url)
            // 发送
            xhr.send()
            //绑定事件
            xhr.onreadystatechange = ()=>{
                if(xhr.readyState === 4){
                    if(xhr.status >= 200 && xhr.status <300){
                        //成功
                        resolve({
                            config: config,
                            data: xhr.response,
                            headers: xhr.getAllResponseHeaders(),
                            request: xhr,
                            status: xhr.status,
                            statusText: xhr.statusText
                        })
                    }else{
                        // 失败
                        reject(new Error("请求失败, 失败的请求码是"+ xhr.status))
                    }
                }
            }


        })
    }
    let axios =  Axios.prototype.request.bind(null)
    //    使用axios函数发现请求,  实际上是调用  Axios.prototype.requsert  
       axios({
           method:"GET",
           url:"http://localhost:3000/posts"
       }).then(response =>{
           console.dir(response);
       })
  • xhr请求只写了成功返回。

image.png

拦截器原理

  • Axios.prototype.request在前面已经说了,下面是InterceptorManager.js,
function InterceptorManager() {
  this.handlers = [];
}

InterceptorManager.prototype.use = function use(fulfilled, rejected, options) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected,
    // pr后的两个属性,用户设置拦截器同步或异步, 默认是同步
    synchronous: options ? options.synchronous : false,
    runWhen: options ? options.runWhen : null
  });
  return this.handlers.length - 1;
};

InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};

InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) {
      fn(h);
    }
  });
};
function Axios(instanceConfig) {
  // 将指定的config, 保存为defaults属性 
  this.defaults = instanceConfig;
  // 将包含请求/响应拦截器管理器的对象保存为interceptors属性
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
  • interceptors上有requestresponse两个属性,两个属性都是InterceptorManager的实例,实例对象上有一个headers数组、原型对象上有一个use方法,通过use方法将用户定义的请求拦截器的成功和失败的两个回调压入到headers数组头部,如果有多个请求拦截器,每一个拦截器在上一个拦截器前面,类似先进后出。响应拦截器同样也是通过use方法添加,但他采用类似先进先出,每次都压入数组尾部。

图片来自腾讯云社区pingan8787作者视频截图 image.png

  • 当请求来到请求拦截器时,会将数组中前两个元素取出(成功回调和失败回调),处理参数返回的promise状态是成功,执行成功回调,状态是失败就执行失败回调,我们可以在拦截器回调中判断请求是否满足条件,满足放行,进行下一个拦截器成功回调或dispatchRequset,失败进行下一个拦截器失败回调或undefined。成功回调成功执行下一个成功回调,失败执行下一个失败回调。
  • 响应拦截器同样也是,不过最终返回给请求axios().then()处理结果。 流程: 请求拦截器2 => 请求拦截器 1 => 发ajax请求 => 响应拦截器1 => 响 应拦截器 2 => 请求的回调

拦截器模拟

     //构造函数
        function Axios(config) {
            this.config = config
            this.interceptors = {
                response: new InterceptorManger(),
                request: new InterceptorManger()
            }
        }
        Axios.prototype.request = function (config) {
            let promise = Promise.resolve(config)

            const chains = [dispatchRequest, undefined]
            //请求拦截器, 将请求拦截器的回调压入到chains的前面
            this.interceptors.request.handers.forEach(item =>{
                chains.unshift(item.fulfiled, item.reject)
            })
            //响应拦截器, 将响应拦截器的回调压入到chains的后面
            this.interceptors.response.handers.forEach(item => {
                chains.push(item.fulfiled, item.reject)
            })
            // 遍历
            while(chains.length>0){
                promise = promise.then(chains.shift(), chains.shift())
            }
            return promise
        }
        function dispatchRequest(){
            // 返回一个promise对象
            return new Promise((resolve, reject) => {
                resolve({
                    status: 200,
                    statusText: "OK"
                })
            })
        }

        // 拦截器管理器构造函数
        function InterceptorManger() {
            this.handers = []
        }
        InterceptorManger.prototype.use = function (fulfiled, reject) {
            this.handers.push({
                fulfiled,
                reject
            })
        }

        // 创建axios函数
        let context = new Axios()
        let axios = Axios.prototype.request.bind(context)
        Object.keys(context).forEach(key =>{
            axios[key] = context[key]
        })
        
        //下面是测试代码
        //设置请求拦截器
        axios.interceptors.request.use(function one(config) {
            console.log("请求拦截器 成功 -1号");
            config.params = { a: 100 }
            
            return config
        }, function (error) {
            console.log("请求拦截器 失败 -1号");
            return Promise.reject(error)
        })
        axios.interceptors.request.use(function two(config) {
            console.log("请求拦截器 成功 -2号");
            config.timeout = 2000
            return config
        }, function (error) {
            console.log("请求拦截器 失败 -2号");
            return Promise.reject(error)
        })

        //设置响应拦截器
        axios.interceptors.response.use(function one(response) {
            console.log("响应拦截器 成功 -1号");

            return response
        }, function (error) {
            console.log("响应拦截器 失败 -1号");
            return Promise.reject(error)
        })
        axios.interceptors.response.use(function two(response) {
            console.log("响应拦截器 成功 -2号");

            return response
        }, function (error) {
            console.log("响应拦截器 失败 -2号");
            return Promise.reject(error)
        })
        

        //发送请求
        axios({
            method: "GET",
            url:"http://localhost:3000/posts" 
        }).then(response => {
            console.log(response);
        })
  • 默认执行成功回调

image.png

取消请求原理

  • 请求取消需要在config中传入一个cancelToken属性,该属性是一个CancelToken实例对象(axios.CancelToken)
axios({
    method: "GET",
    url: "http://localhost:3000/posts",
    cancelToken: new CancelToken(function (c) {
                        cancel = c
                 })
 })
  • CancelToken.js中CancelToken构造函数
// CancelToken构造函数
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  // 为取消请求准备一个promise对象, 并保存resolve函数
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  // 保存当前token对象
  var token = this;

  // 立即执行接收的执行器函数, 并传入用于取消请求的cancel函数
  executor(function cancel(message) {
    // 如果token中有reason了, 说明请求已取消
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }
    // 将token的reason指定为一个Cancel对象
    token.reason = new Cancel(message);
    // 将取消请求的promise指定为成功, 值为reason
    resolvePromise(token.reason);
  });
}
  • xhr.js
    // 如果配置了cancelToken
    if (config.cancelToken) {
      // 指定用于中断请求的回调函数
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }
        // 中断请求
        request.abort();
        // 让请求的promise失败
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
  • 取消请求的核心是 xhr.abort()方法,所以需要明白什么时候调用就行。
  • CancelToken构造函数中,给CancelToken对象实例上添加了一个promise属性,该属性是一个promise对象,并且将该对象的改变状态的resolve方法赋值给一个变量resolvePromise。executor函数是用户存入的config中CancelToken属性的CancelToken实例对象传入的回调函数function(c){...},而executor()中的函数赋值给cancel,CancelToken实例对象添加一个reason属性(boolean)。
  • dispatchRequest.js调用
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}
module.exports = function dispatchRequest(config) {
 /* 
  如果请求已经被取消, 直接抛出异常
  */
  throwIfCancellationRequested(config);
  ...
}
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};
  • 然后通过用户点击按钮之类的取消请求执行cancel函数,然后改变了CancelToken对象实例promise属性的状态,状态改变xhr.jsconfig.cancelToken.promise.then()执行第一个参数-回调函数,结果request.abort()执行,请求取消。

模拟请求取消

  • 在模拟前需要重新起到json-server,在命令行中输入json-server --w db.json -d 2000,让请求延时2秒,否则请求成功太快,取消来不及
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="container">
        <h2 class="page-header">axios取消请求</h2>
        <button class="btn btn-primary">发送请求</button>
        <button class="btn btn-warning">取消请求</button>
    </div>
    <script>
        // 构造函数
        function Axios(config) {
            this.config = config
        }
        //  原型 request 方法
        Axios.prototype.request = function (config) {
            return dispatchRequest(config)
        }
        // dispatchRequest函数
        function dispatchRequest(config) {
            return xhrAdapter(config)
        }
        // xhrAdapter函數
        function xhrAdapter(config) {
            // 发送AJAX请求
            return new Promise((resolve, reject) => {
                // 实例化对象
                let xhr = new XMLHttpRequest()
                // 初始化
                xhr.open(config.method, config.url)
                // 发送
                xhr.send()
                // 处理结果
                xhr.onreadystatechange = function () {
                    if (xhr.readyState === 4) {
                        // 判断结果
                        if (xhr.status >= 200 && xhr.status < 300) {
                            resolve({
                                status: xhr.status,
                                statusText: xhr.statusText
                            })
                        } else {
                            reject(new Error("请求失败"))
                        }
                    }
                }
                // 取消请求处理
                if (config.cancelToken) {
                    // 对cancelToken  对象身上的promise对象指定成功的回调
                    config.cancelToken.promise.then(value => {
                        xhr.abort()
                        reject(new Error("请求已经取消"))
                    })
                }
            })

        }

        // CancelToken 构造函数
        function CancelToken(executor) {
            var resolvePromise;
            // 为实例对象添加属性
            this.promise = new Promise((resolve) => {
                resolvePromise = resolve
            })
            // 调用executor
            executor(function () {
                resolvePromise()
            })
        }
        //以下是试验
        let context = new Axios()
        let axios = Axios.prototype.request.bind(context)

        let cancel = null
        const btns = document.querySelectorAll("button")

        btns[0].onclick = function () {
            axios({
                method: "GET",
                url: "http://localhost:3000/posts",
                cancelToken: new CancelToken(function (c) {
                    cancel = c
                })
            }). then(response => {
                console.log(response);
                cencel = null
            })
        }
        btns[1].onclick = function () {
               cancel()
        }
    </script>
</body>
</html>

image.png

总结

看axios源码我体会颇深,看着一行行代码,一个个不认识的单词,多个文件转来转去,真让人头大,但静下心来看,确实能学习到很多,不仅仅是源码内容,还有的是作者各种聪明的设计,例如拦截器的设计,promise链式骨架,使用数组进行压入压出,浅显易懂。例如取消请求中创建一个promise对象属性,通过对象状态的改变来判断是否取消请求。还有...

本文对axios基本功能的解读和模拟,如果对你有用,希望你能动下小手点个赞,也欢迎各位评论区指正。