写在前面

  • 本文主要是介绍前端开发过程中对于Axios的基础使用以及并且了解其axios的源码实现后手写部分axios内置api方法加深对其理解;
  • 阅读本文你能够了解到为什么在使用Axios发送请求时:
  • 我们能够通过函数调用的方式【Axios(config)】又或是函数对象的方式【Axios.xxx(config)】指定请求方式便捷的发起请求;
  • 请求拦截器和响应拦截器的执行顺序原理;
  • axios是如何实现取消请求;
  • 在使用Axios之前,需要提前了解es6中Promise以及XMLHttpRequest。

一、Axios是什么?有什么特点?

  • Axios是一个基于Promise的网络请求库,作用于nodejs和浏览器中;在服务端使用原生node中http模块发送请求,浏览器中使用XMLHttpRequest请求;
  • 特点:
  • 1.拦截请求和响应;2.转换请求和响应数据;3.取消请求;4.自动转换json数据;5.客户端支持XSRF(跨站请求伪造)

二、简单服务搭建以及axios的基本使用

1使用json-server搭建简单服务器

  • 使用它创建一些简单的数据,以及模拟后端接口返回
npm install -g json-server
  • 创建一个文件夹(这里叫server),初始化package.json文件
npm init -y
  • 在该文件夹下创建一个json文件,里边写点假数据用于后续测试
{
  "title": [
    {
      "id": 1,
      "title": "这是标题",
      "author": "polaris"
    }
  ],
  "desc": [
    {
      "id": 1,
      "body": "北极星超级帅。。。",
      "postId": 1
    }
  ],
  "intro": {
    "name": "hahhaaa"
  }
}
  • 启动服务,npx(Node 程序包运行器)
npx json-server data.json
  • 执行之后控制台可以看到,如下图,左边附上目录结构;浏览器输入地址就能看到输出了。

axios样例 axios详解_axios样例

  • 这里我们可以吧启动命令在package.json中配置一下,不然每次敲一行有点麻烦,也不符合我们的开发习惯;所以我们打开package.json文件,在scripts对象中新增执行命令;
  • --watch简写(-w)标识监听data.json的改变,在文件内容发送变化后自动执行服务,保证接口返回的是最新的数据;
{
  "name": "server",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    //新增此行
    "serve": "json-server --watch data.json",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
  • 现在我们启动服务就只需要使用命令:
npm run serve
  • 以上步骤我们搭建了一个简单的服务,现在开始引入axios进行基本的使用

2使用axios

  • 与server文件夹平级创建src目录,新建index.html文件
  • 在index.html中使用script链接的方式引入axios进行使用,链接点这里

axios样例 axios详解_ajax_02

  • 浏览器中查看

axios样例 axios详解_javascript_03

  • 除了get方法外还有post,put等方法,可以自行尝试;

3 配置补充

3.1 请求别名方式调用

  • 上面请求写法可以看到,我们可以使用axios(config),config为传入的配置对象或是axios.get这样的方式直接指定该请求的类型;
  • 更多:
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.options(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])

3.2 请求配置

  • 也就是axios(config)中的config配置对象都有哪些可以自定义的属性:
{
  // `url` 是用于请求的服务器 URL
  url: '/user',

  // `method` 是创建请求时使用的方法
  method: 'get', // 默认值

  // `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
  // 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
  baseURL: 'https://some-domain.com/api/',

  // `transformRequest` 允许在向服务器发送前,修改请求数据
  // 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
  // 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
  // 你可以修改请求头。
  transformRequest: [function (data, headers) {
    // 对发送的 data 进行任意转换处理

    return data;
  }],

  // `transformResponse` 在传递给 then/catch 前,允许修改响应数据
  transformResponse: [function (data) {
    // 对接收的 data 进行任意转换处理

    return data;
  }],

  // 自定义请求头
  headers: {'X-Requested-With': 'XMLHttpRequest'},

  // `params` 是与请求一起发送的 URL 参数
  // 必须是一个简单对象或 URLSearchParams 对象
  params: {
    ID: 12345
  },

  // `paramsSerializer`是可选方法,主要用于序列化`params`
  // (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
  paramsSerializer: function (params) {
    return Qs.stringify(params, {arrayFormat: 'brackets'})
  },

  // `data` 是作为请求体被发送的数据
  // 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
  // 在没有设置 `transformRequest` 时,则必须是以下类型之一:
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属: FormData, File, Blob
  // - Node 专属: Stream, Buffer
  data: {
    firstName: 'Fred'
  },
  
  // 发送请求体数据的可选语法
  // 请求方式 post
  // 只有 value 会被发送,key 则不会
  data: 'Country=Brasil&City=Belo Horizonte',

  // `timeout` 指定请求超时的毫秒数。
  // 如果请求时间超过 `timeout` 的值,则请求会被中断
  timeout: 1000, // 默认值是 `0` (永不超时)

  // `withCredentials` 表示跨域请求时是否需要使用凭证
  withCredentials: false, // default

  // `adapter` 允许自定义处理请求,这使测试更加容易。
  // 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
  adapter: function (config) {
    /* ... */
  },

  // `auth` HTTP Basic Auth
  auth: {
    username: 'janedoe',
    password: 's00pers3cret'
  },

  // `responseType` 表示浏览器将要响应的数据类型
  // 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
  // 浏览器专属:'blob'
  responseType: 'json', // 默认值

  // `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
  // 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
  // Note: Ignored for `responseType` of 'stream' or client-side requests
  responseEncoding: 'utf8', // 默认值

  // `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
  xsrfCookieName: 'XSRF-TOKEN', // 默认值

  // `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
  xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值

  // `onUploadProgress` 允许为上传处理进度事件
  // 浏览器专属
  onUploadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `onDownloadProgress` 允许为下载处理进度事件
  // 浏览器专属
  onDownloadProgress: function (progressEvent) {
    // 处理原生进度事件
  },

  // `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
  maxContentLength: 2000,

  // `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
  maxBodyLength: 2000,

  // `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
  // 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
  // 则promise 将会 resolved,否则是 rejected。
  validateStatus: function (status) {
    return status >= 200 && status < 300; // 默认值
  },

  // `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
  // 如果设置为0,则不会进行重定向
  maxRedirects: 5, // 默认值

  // `socketPath` 定义了在node.js中使用的UNIX套接字。
  // e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
  // 只能指定 `socketPath` 或 `proxy` 。
  // 若都指定,这使用 `socketPath` 。
  socketPath: null, // default

  // `httpAgent` and `httpsAgent` define a custom agent to be used when performing http
  // and https requests, respectively, in node.js. This allows options to be added like
  // `keepAlive` that are not enabled by default.
  httpAgent: new http.Agent({ keepAlive: true }),
  httpsAgent: new https.Agent({ keepAlive: true }),

  // `proxy` 定义了代理服务器的主机名,端口和协议。
  // 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
  // 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
  // `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
  // 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
  // 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
  proxy: {
    protocol: 'https',
    host: '127.0.0.1',
    port: 9000,
    auth: {
      username: 'mikeymike',
      password: 'rapunz3l'
    }
  },

  // see https://axios-http.com/zh/docs/cancellation
  cancelToken: new CancelToken(function (cancel) {
  }),

  // `decompress` indicates whether or not the response body should be decompressed 
  // automatically. If set to `true` will also remove the 'content-encoding' header 
  // from the responses objects of all decompressed responses
  // - Node only (XHR cannot turn off decompression)
  decompress: true // 默认值

}

3.3 响应结构

  • 这就是发起请求之后返回的响应对象结构
{
  // `data` 由服务器提供的响应,会自动解析为json格式
  data: {},

  // `status` 来自服务器响应的 HTTP 状态码
  status: 200,

    // `statusText` 来自服务器响应的 HTTP 状态信息
    statusText: 'OK',
    
    // `headers` 是服务器响应头
    // 所有的 header 名称都是小写,而且可以使用方括号语法访问
    // 例如: `response.headers['content-type']`
    headers: {},

  // `config` 是 `axios` 请求的配置信息
  config: {},

  // `request` 是生成此响应的请求
  // 在node.js中它是最后一个ClientRequest实例 (in redirects),
  // 在浏览器中则是 XMLHttpRequest 实例
  request: {}
}

3.4 默认的全局配置

  • 例如get请求方式,这是默认的请求类型,所以下面的method属性为get时可以省略
axios({
  method: 'get', //默认值
  url: 'http://localhost:3000/title',
}).then((res) => {
  console.log(res)
})
  • 其余默认配置
//请求基础地址,比如设置为如下,那么我们在配置url属性的时候就只需要写基础地址之后的内容
//url: 'http://localhost:3000/title' => url: /title
axios.defaults.baseURL = 'https://localhost:3000';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
//Content-Type来表示具体请求中的媒体类型信息,确切的来说是客户端告知服务端,
//自己即将发送的请求消息携带的数据结构类型,好让服务端接收后以合适的方式处理。
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
//请求类型
axios.defaults.method = 'get';

3.5 自定义默认实例

  • 通过axios.create方法创建自定义实例,每个实例都能够独立的配置,比如默认域名地址等等
// 创建实例时配置默认值
const instance = axios.create({
  baseURL: 'https://localhost:3000'
});
const instance = axios.create({
  baseURL: 'https://localhost:9000'
});
// 创建实例后修改默认值
instance.defaults.headers.common['Authorization'] = AUTH_TOKEN;

//使用起来
instance({
  method: 'get',
  url: '/title/1'
}).then(res => {
  console.log(res)
})

3.6 拦截器

  • 请求与响应拦截器分别有成功与失败的回调函数
  • 当请求拦截到请求失败的时候,对应的响应拦截器执行的就是响应失败的回调
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 超出 2xx 范围的状态码都会触发该函数。
    // 对响应错误做点什么
    return Promise.reject(error);
  });

3.7 取消请求

  • 为了测试请求取消,需要设置package.json中的服务命令,新增2s延时方便测试
{
  //...
  "scripts": {
    "serve": "json-server --watch data.json -d 2000"
  }
}
  • 测试例子,点击发送请求后在2s内点击取消请求;
<!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>
    <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.2.0/axios.min.js"></script>
  </head>
  <body>
    <button>发起请求</button>
    <button>quxiao 请求 post</button>
    <script>
      const CancelToken = axios.CancelToken
      let btn = document.querySelectorAll('button')
      let cancel = null

      btn[0].onclick = function () {
        if (cancel !== null) return
        axios({
          url: 'http://localhost:3000/title',
          cancelToken: new CancelToken(function executor(c) {
            // executor 函数接收一个 cancel 函数作为参数
            cancel = c
          }),
        }).then((res) => {
          console.log(res)
          cancel = null
        })
      }
      btn[1].onclick = function () {
        cancel()
      }
      
    </script>
  </body>
</html>

4 简单总结

  • 以上内容我们简单搭建了一个服务,并且通过script引入axios测试了一些例子;
  • 接下来我们根据axios库模拟实现一些它的api;

三、源码理解以及手写部分api

1 核心目录文件

  • 安装axios依赖,这样能够在node_modules目录中查看axios的源码文件
npm i axios
  • 打开lib文件夹

axios样例 axios详解_前端_04

1.1 adapters

  • 定义请求的适配器 xhr, http
  • http.js用于在node服务中向远端发送http请求
  • xhr.js用于前端发送ajax请求

1.2 cancel

  • 取消事件相关的文件
  • CancelToken.js 存放这取消请求的构造函数,使用起来 new axios.CancelToken()

1.3 core目录

  • 核心文件
  • Axios构造函数

2 实现axios的创建过程

2.1 基本认知

  • axios在使用时,既可以当做函数调用也可以通过对象的方式使用,形如: axios(config),axios.get(config);axios支持我们自定义默认配置(例如设置默认请求方式method、基础路径baseUrl等等l)以及请求响应拦截
  • 除了上面说的axios的两种方式发送请求,还能通过const instance = axios.create(config) 的方式创建axios实例,使用起来 instance(config),instance.get(config) 与axios全局对象完全一致;既然使用方式相同区别又在哪呢?
  • 首先这两种方式本质内部都是调用createInstance函数生成axios实例;
  • 不同点为使用axios.create可以创建多个自定义实例instance,我们可以为每个instance进行单独的配置,例如baseUrl,get等等,它是多例模式;而全局的axios只有一个,是单例模式;
  • 并且axios身上还具备CanceledError、CancelToken等属性方法,而通过axios.create方法创建出的axios实例则是没有这些属性方法。

2.2 具体实现

2.2.1 Axios构造函数
  • 这里我们先初始化一个Axios构造函数,并向器原型对象添加request,get等方法
  • 其中request方法用于发送ajax请求,get,post等请求本质都是调用此方法发送请求
// Axios构造函数
function Axios(config) {
  // 接收默认属性,例如将请求方法默认设置为get等
  this.defaults = config
  // 拦截器
  this.interceptors = {
    //请求拦截器
    request: {},
    //响应拦截器
    response: {},
  }
}
// 向Axios原型添加请求方法,其他方法发送请求都是内部调用此方法
Axios.prototype.request = function (config) {
  console.log('发送ajax请求,类型为', config.method)
}
Axios.prototype.get = function (config) {
  return this.request({ method: 'get' })
}
Axios.prototype.post = function (config) {
  return this.request({ method: 'post' })
}
2.2.2 createInstance函数
  • createInstance函数,用于创建axios实例(这就是暴露给页面使用的axios实例对象)
  • 内部主要逻辑是:
  • 实例化一个Axios对象(context),此对象包含Axios函数原型的所有方法和属性,以及defaults和interceptors属性
  • 通过调用bind方法创建一个request函数的拷贝,绑定函数调用时的this指向为context;将此函数赋值给instance变量,即instance成为一个与request功能相当的函数
  • 遍历Axios的原型对象,将它身上的方法复制一份到instance对象身上,这样一来,instance函数就不仅可以通过函数的方式调用,同时也支持对象方法的形式调用;形如 instance(config),instance.get(config)
  • 最后,从Axios实例对象context上复制defaults,interceptors属性到instance对象上,至此,instance具备了完整的功能
// 用于创建Axios实例对象
function createInstance(config) {
  // 实例化一个对象,该对象继承Axios所有属性方法
  const context = new Axios(config)
  // 通过bind方法返回一个request函数,将this绑定为Axios实例对象(context)
  // 即instance是一个具备request功能的函数了
  const instance = Axios.prototype.request.bind(context)
  // 将Axios原型对象(prototype)的方法添加到instance函数上
  // 这样instance函数对象能够通过instance.xxx的方式调用请求
  Object.keys(Axios.prototype).forEach(key => {
    // 并且函数绑定context对象,保证this指向正确
    instance[key] = Axios.prototype[key].bind(context)
  })
  // 为instance对象添加 defaults,interceptors属性
  Object.keys(context).forEach(key => {
    instance[key] = context[key]
  })
 // 给instance对象添加create属性,也就是create方法,该方法内部调用createInstance方法
 // 返回axios实例对象;这样页面能够以 axios.create 方法返回一个axios实例
 instance.create = function create(otherConfig) {
  //TODO 关于对象合并的操作没有处理完,这里简单介绍一下,所以使用Object.assign
   return createInstance(Object.assign(config || {}, otherConfig || {}))
 }
  //返回这个函数
  return instance
}
2.2.3 测试例子
<!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>
    <!-- <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.2.0/axios.min.js"></script> -->
  </head>
  <body>
    <script>
      // Axios构造函数
      function Axios(config) {
        // 默认属性,例如将请求方法默认设置为get等
        this.defaults = config
        // 拦截器
        this.interceptors = {
          request: {},
          response: {},
        }
      }
      // 向Axios原型添加请求方法,其他方法发送请求都是内部调用此方法
      Axios.prototype.request = function (config) {
        console.log('发送ajax请求,类型为', config.method)
      }
      Axios.prototype.get = function (config) {
        return this.request({ method: 'get' })
      }
      Axios.prototype.post = function (config) {
        return this.request({ method: 'post' })
      }

      // 用于创建Axios实例对象
      function createInstance(config) {
        // 实例化一个对象,该对象继承Axios所有属性方法
        const context = new Axios(config)
        // 通过bind方法返回一个request函数,将this绑定为Axios实例对象(context)
        // 即instance是一个具备request功能的函数了
        const instance = Axios.prototype.request.bind(context)
        // 将Axios原型对象(prototype)的方法添加到instance函数上
        // 这样instance函数对象能够通过instance.xxx的方式调用请求
        Object.keys(Axios.prototype).forEach((key) => {
          // 并且函数绑定context对象,保证this指向正确
          instance[key] = Axios.prototype[key].bind(context)
        })
        // 为instance对象添加 defaults,interceptors属性
        Object.keys(context).forEach((key) => {
          instance[key] = context[key]
        })
        return instance
      }

      // 测试
      const axios = createInstance()
      axios({method: 'get'})
      axios.get()
    </script>
  </body>
</html>

3 实现axios发送请求

3.1 基本流程

  • 使用axios(config)发送get请求为例子
  • axios内部请求最终是调用request方法发送请求,该方法会返回一个promsie对象
  • request函数内部首先创建一个成功的promsie实例,并且指定成功的回调函数为dispatchRequest,失败的回调为undefined;其中dispatchRequest函数内部调用xhrAdapter方法,该方法内部通过XMLHttpRequest向后端服务发送请求,并将结果存入到promsie实例当中返回
  • 即dispatchRequest函数内部调用xhrAdapter方法,由xhrAdapter发起请求后将结果返回给dispatchRequest函数,最终request函数中promsie指定的成功回调拿到dispatchRequest函数返回的结果

3.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>
    <!-- <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.2.0/axios.min.js"></script> -->
  </head>
  <body>
    <script>
      // Axios构造函数
      function Axios(config) {
        // 默认属性,例如将请求方法默认设置为get等
        this.defaults = config
      }

      // 向Axios原型添加请求方法,其他方法发送请求都是内部调用此方法
      Axios.prototype.request = function (config) {
        // 创建一个成功的promsie
        const promise = Promise.resolve(config)
        // 声明数组,存入请求回调和一个undefined(用于占位)
        const chains = [dispatchRequest, undefined]
        // // 将dispatchRequest函数作为promise的成功回调
        const result = promise.then(chains[0], chains[1])
        // // 返回promise结果值
        return result
        // console.log(promise)
      }

      // dispatchRequest函数,这里调用xhrAdapter函数发送请求
      function dispatchRequest(config) {
        // 调用xhr适配器发送请求
        return xhrAdapter(config).then(
          (res) => {
            // todo 对响应结果进行转换处理
            return res
          },
          (err) => {
            return err
          }
        )
      }

      // xhr adapter适配器函数,这里用于发送ajax请求
      function xhrAdapter(config) {
        return new Promise((reslove, reject) => {
          const xhr = new XMLHttpRequest()
          xhr.open(config.method, config.url)
          xhr.send(null)
          xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
              if (xhr.status >= 200 && xhr.status < 300) {
                reslove({
                  config: config, //配置对象
                  data: xhr.response, //响应体
                  headers: xhr.getAllResponseHeaders(), //字符串
                  request: xhr, //xhr请求对象
                  status: xhr.status, //响应状态码
                  statusText: xhr.statusText, //响应状态字符串
                })
              } else {
                  reject(new Error('失败了:', xhr.status))
                }
            } 
          }
        })
      }

      // 创建axios函数
      const axios = Axios.prototype.request.bind(null)

      axios({ method: 'GET', url: 'http://localhost:3000/title' }).then(
        (res) => {
          console.log('res', res)
        }
      )
      // axios.get()
    </script>
  </body>
</html>

4 实现axios拦截器的功能

  • 拦截器构造函数为InterceptorManager,并且interceptors对象中的request、response属性值均为InterceptorManager实例对象
  • InterceptorManager函数实例上存在handlers数组,用于存储拦截器的回调函数;函数原型上存在use方法,该方法用于将请求、响应拦截的回调存入handlers

4.1 代码实现以及测试例子

  • 这个例子能够说明为什么多个定义了多个拦截器时,拦截器的执行顺序总是先请求拦截器再执行响应拦截器
<!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>
    <!-- <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.2.0/axios.min.js"></script> -->
  </head>
  <body>
    <script>
      function Axios(config) {
        this.config = config
        this.interceptors = {
          request: new InterceptorManager(),
          response: new InterceptorManager()
        }
      }
      // 拦截器构造函数
      function InterceptorManager() {
        this.handlers = []
      }
      // use方法,用于存储拦截器的回调函数
      InterceptorManager.prototype.use = function(fulfilled, rejected) {
        this.handlers.push({
          fulfilled,
          rejected
        })
      }

      Axios.prototype.request = function(config) {
        // 创建promsie对象
        let promise = Promise.resolve(config)
        // 创建一个数组,放入请求函数和undefined占位,因为执行过程是每次取两个函数
        let chains = [dispatchRequest, undefined]
        // 处理拦截器
        // 请求拦截器,循环遍历请求拦截器的回调,添加到chains数组头部
        this.interceptors.request.handlers.forEach(item => {
          chains.unshift(item.fulfilled, item.rejected)
        })
        // 响应拦截器,循环遍历请求拦截器的回调,添加到chains数组末尾
        this.interceptors.response.handlers.forEach(item => {
          chains.push(item.fulfilled, item.rejected)
        })
        // console.log(chains)
        // 遍历执行chains中的回调,从数组顶部依次取出执行
        while(chains.length > 0) {
          // 每次取出两个回调,分别指定为成功,失败
          // 当成功执行后会返回一个新的promise,接着执行
          promise = promise.then(chains.shift(), chains.shift())
        }
        return promise
      }

      // 简单默认发送请求
      function dispatchRequest() {
        return new Promise((reslove, reject) => {
          reslove({
            status: 200,
            statusText: 'ok'
          })
        })
      }

      const context = new Axios({})
      const axios = Axios.prototype.request.bind(context)
      Object.keys(context).forEach(key => {
        axios[key] = context[key]
      })
      
      // 添加请求拦截器,测试例子
      // use执行时将回调函数存入handers数组当中
      axios.interceptors.request.use(
        function one(config) {
          console.log('请求拦截器成功--111')
          return config
        },
        function one(error) {
          console.log('请求拦截器失败-111')
          return Promise.reject(error)
        }
      )

      // 添加响应拦截器
      axios.interceptors.response.use(
        function one(response) {
          console.log('响应拦截器成功--111')
          // 2xx 范围内的状态码都会触发该函数。
          return response
        },
        function one(error) {
          console.log('响应拦截器失败--111')
          // 超出 2xx 范围的状态码都会触发该函数。
          return Promise.reject(error)
        }
      )

      // 添加请求拦截器
      axios.interceptors.request.use(
        function two(config) {
          console.log('请求拦截器成功--222')
          return config
        },
        function two(error) {
          console.log('请求拦截器失败-222')
          return Promise.reject(error)
        }
      )

      // 添加响应拦截器
      axios.interceptors.response.use(
        function two(response) {
          console.log('响应拦截器成功--222')
          // 2xx 范围内的状态码都会触发该函数。
          return response
        },
        function two(error) {
          console.log('响应拦截器失败--222')
          // 超出 2xx 范围的状态码都会触发该函数。
          return Promise.reject(error)
        }
      )

      console.dir(axios)

      axios({
        method: 'get',
        url: 'http://localhost:3000/title'
      }).then(res => {
        console.log('res...', res)
      })
    </script>
  </body>
</html>

5 实现axios 取消请求功能

  • 主要是靠CancelToken构造函数实现,在实例化CancelToken对象时,需要传入一个带形参的回调函数,这个形参用来接收CancelToken函数内部promise的成功回调
  • CancelToken函数实例化时会创建一个临时变量reslovePromise(用于存储回调函数);实例对象上会生成一个属性(这里指promise),并且实例化一个promsie对象赋值给它,这个promise对象的成功回调赋值给临时变量reslovePromise;最后会调用传入的回调函数,并将reslovePromise通过函数的方式暴露给外部;到这里实际就是把promise的状态变更权利交给了外部函数,当执行这个函数时promise状态变更为fulfilled,进而在发送ajax请求(xhrAdapter方法内)时通过状态变化来终止请求xhr.abort
<!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>
    <!-- <script src="https://cdn.bootcdn.net/ajax/libs/axios/1.2.0/axios.min.js"></script> -->
  </head>
  <body>
    <button>发起请求</button>
    <button>取消请求</button>

    <script>
      function Axios(config) {
        this.config = config
      }

      Axios.prototype.request = function (config) {
        return dispatchRequest(config)
      }

      function dispatchRequest(config) {
        return xhrAdapter(config)
      }

      // 发送ajax请求
      function xhrAdapter(config) {
        return new Promise((reslove, reject) => {
          const 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) {
                reslove({
                  status: xhr.status,
                  statusText: xhr.statusText,
                })
              } else {
                reject(new Error('请求失败!'))
              }
            }
          }
          // 取消请求判断
          if (config.cancelToken) {
            // 状态改变执行成功回调,那么取消请求
            config.cancelToken.promise.then((reslove) => {
              xhr.abort()
            })
          }
        })
      }

      const context = new Axios({})
      const axios = Axios.prototype.request.bind(context)

      // CancelToken构造函数
      function CancelToken(executor) {
        // 声明变量,用于存储reslove暴露给外部
        let reslovePromise
        // 给CancelToken实例对象添加一个promise属性,值为promise对象
        // 将成功的回调(reslove)赋值给reslovePromise变量
        this.promise = new Promise((reslove) => {
          reslovePromise = reslove
        })

        // 调用外部传入的函数
        executor(function () {
          // 函数内部执行promise成功的回调,改变promise状态为成功
          reslovePromise()
        })
      }
      // console.dir(axios)

      const btns = document.querySelectorAll('button')
      // 临时变量,接收promise的成功回调
      let cancel = null

      btns[0].onclick = function () {
        if (cancel !== null) return
         
        // 实例化一个CancelToken对象,传入一个回调函数
        // 构造函数内部会执行这个回调,并且把promise成功回调通过形参c赋值给cancel
        // 外部通过调用cancel函数执行reslove函数从而改变promise状态
        const cancelToken = new CancelToken(function (c) {
          cancel = c
        })

        axios({
          method: 'get',
          url: 'http://localhost:3000/title',
          cancelToken: cancelToken,
        }).then((res) => {
          console.log(res)
          cancel = null
        })
      }
      btns[1].onclick = function() {
        cancel()
      }
    </script>
  </body>
</html>