阅读 447

三步法解析Axios源码

关注公众号“执鸢者”,回复“红宝书”获取“javaScript高级程序第四版(pdf)”及大量前端学习资料。

一、领悟思想

Axios是一个基于Promise的HTTP库,根据官网介绍,有以下几个特点:

  1. 在浏览器端会创建XMLHttpRequests
  2. 在Node端会创建HTTP请求
  3. 由于Axios是一个基于Promise的HTTP库,所以其支持Promise API
  4. 支持请求和响应拦截器
  5. 支持请求和响应数据转换
  6. 支持取消请求
  7. 自动转换JSON数据
  8. 客户端支持防御XSRF攻击

通过上述官网介绍的特点,我认为其突出的优点有三个:

  1. 支持Promise API,可以方便进行链式调用;
  2. 支持请求和响应拦截器,该拦截器将Node中中间件思想引入该库,在请求发送之前和响应接收之后可以对其进行处理。
  3. 支持数据转换器,转换器主要负责数据发送前以及响应接收后对数据的处理。

二、把握设计

理解了该库设计的特点,下面从源码目录、抽象接口及核心设计原理三个层面对Axios进行整体的把握。

2.1 源码目录

如下所示是Axios的源码目录及各个文件的作用

2.2 抽象接口

对源码的目录有了一定了解,下面利用UML类图对该系统各个模块的依赖关系进一步了解,为后续源码分析打好基础。(看该图注意对着源码一起看)

2.3 设计原理

首先看一段代码,这段代码的执行顺序包含着Axios的核心原理。

axios.defaults.baseURL = 'http://localhost:8080'

// 请求拦截器一
axios.interceptors.request.use(
    config => {
        console.log('请求拦截器一', config);
        return config;
    },
    error => {
        console.log('request interceptor rejected1');
        return Promise.reject(error);
    }
);

// 请求拦截器二
axios.interceptors.request.use(
    config => {
        console.log('请求拦截器二', config);
        return config;
    },
    error => {
        console.log('request interceptor rejected2');
        return Promise.reject(error);
    }
);

// 响应拦截器一
axios.interceptors.response.use(
    response => {
        console.log('响应拦截器一', response);
        return response;
    },
    error => {
        console.log('response interceptor rejected1');
        return Promise.reject(error);
    }
);

// 响应拦截器二
axios.interceptors.response.use(
    response => {
        console.log('响应拦截器二', response);
        return response;
    },
    error => {
        console.log('response interceptor rejected2');
        return Promise.reject(error);
    }
);

axios('/', {
    method: 'post',
    headers: {
        'Content-Type': 'application/json'
    },
    data: {
        test: 'test'
    },
    // 请求转换器
    transformRequest: [(data, headers) => {
        console.log('请求转换器', data);
        return JSON.stringify(data)
    }],
    // 响应转换器
    transformResponse: [(response, headers) => {
        console.log('响应转换器', response);
        return response;
    }]
})
.then((response) => {
    console.log(response.data)
})
复制代码

写了这么多代码,大家肯定对这段代码的执行结果很感兴趣,为了满足各位看客的好奇心,下面就直接抛出来这段结果。

不过单看执行结果也不能了解其核心设计原理呀,老铁别急,其实小小代码就已经包含了Axios的整个执行过程,通过观察结果及代码可以将整个过程简化为下图:

其核心原理就是这个吗?是的,你没有看错,这就是Axios的核心设计原理,通过一系列链式的处理就能够得到所需要的结果。

三、体会细节

宏观的事聊完了,下面就详细聊几个核心细节吧:整个流程、请求/响应拦截器、dispatchRequest是个啥、请求/响应数据转换器。

3.1 整体运行流程

在第二章中阐述了该核心原理,老铁们一定对该整体是如何运转起来的很感兴趣吧,下面就来解答各位老铁的疑惑——Axios

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 拦截器实例化
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

// 通过一系列的继承绑定操作,该函数其实就是axios函数
Axios.prototype.request = function request(config) {
  // ……
  config = mergeConfig(this.defaults, config);

  // Set config.method
  // ……

  // ****核心****
  // 存储该调用链的数组
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 将请求拦截器的内容塞到数组前面(注意用的unshift函数,这就很好的解释了为什么先调用的请求拦截器后执行)
  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将整个数组中的内容串起来,这样就可以按照顺序链式执行了
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
复制代码

是不是很巧妙?通过利用数组先来存储需要的内容,先处理的在数组的前面(请求拦截器),后处理的在数组的后面(响应拦截器),然后利用Promise将整个内容串起来,很好的处理网络请求属于异步的问题——Perfect。

3.2 请求/响应拦截器

通过观察第二部分的执行结果我们已经了解了请求/响应拦截器,下面就做一下总结:

  1. 请求拦截器就是在发送请求前执行的回调函数,个人认为其最大功用就是对多个请求的配置进行统一修改
  2. 仔细观察发现请求拦截器一先加入但是后执行,是不是与整体运行流程中的代码对上了。
  3. 响应拦截器就是在请求得到响应后执行的回调函数,成功回调的参数就是响应response,其可以对多个请求的响应进行统一修改。

先抛出请求/响应拦截器的核心代码

function InterceptorManager() {
  this.handlers = [];
}

// 注册拦截器
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  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);
    }
  });
};
复制代码

看看拦截器的核心源码,是不是发现与一种设计模式很像?对的,就是观察者模式。当调用use方法的时候就会将回调函数(成功、失败)保存至handlers属性上,方便后期的调用;当调用eject方法的时候就会删除对应索引位置回调函数;当调用forEach方法的时候就会就会对handlers属性(存储的拦截器回调)中的内容进行分发。

3.3 dispatchRequest是个啥

前面聊了整个请求的请求前(请求拦截器)和请求后(响应拦截器),是不是感觉少点东西,如何发请求,这就是我们本次要与大家一起唠的dispatchRequest(config)。

module.exports = function dispatchRequest(config) {
  // ……

  //请求数据转换
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
  // ……
  
  // 获取适配器:自己配置了就选自己的,自己没有设置就选默认的(浏览器端就选xhrAdapter、node端就选httpAdapter;这也就是为什么Axios即支持浏览器又支持Node的原因)
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    // ……

    // 响应数据转换器
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      // ……

      // 响应数据转换器
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};
复制代码

通过观察整个请求流程中的中间环节——dispatchRequest,它一共做了三件事:

  1. 调用请求数据转换器转换请求数据
  2. 选择合适的适配器发起请求——自己配置了就选自己的,自己没有配置就选默认的(浏览器端就选xhrAdapter、node端就选httpAdapter;这也就是为什么Axios即支持浏览器又支持Node的原因)
  3. 当请求数据返回后,调用响应数据转换器转换响应数据

3.4 请求/响应数据转换器

既然3.3中提到了请求/响应转换器,本节就来聊一聊它俩。

// 核心源码
module.exports = function transformData(data, headers, fns) {
  utils.forEach(fns, function transform(fn) {
    data = fn(data, headers);
  });

  return data;
};
复制代码

请求数据转换调用,实质上就是利用请求数据转换器对请求头和请求数据进行特定的处理(transformRequest为处理函数的数组,defaults中包含默认的配置)

config.data = transformData(
  config.data,
  config.headers,
  config.transformRequest
);
复制代码

响应数据转换调用类似于请求数据转换调用,对响应体进行一系列的处理(transformResponse为处理函数的数组,defaults中包含默认的配置)

response.data = transformData(
  response.data,
  response.headers,
  config.transformResponse
);
复制代码

四、结语

上述三章对Axios进行整体的分析,从Axios的特点、整体设计及关键环节三个方面进行了讲述,通过阅读源码学到了很多知识,也能够更加熟练的使用Axios。为了保证各位老铁的学习Axios源码的效果,对学习Axios源码的两条建议:

  1. 边阅读本文边看源码,能够有更深入的理解。
  2. 不要纠结于具体的实现,从宏观的角度去看源码,这样能够节省大量时间。

1.如果觉得这篇文章还不错,来个分享、点赞、吧,让更多的人也看到

2.关注公众号执鸢者,领取学习资料,定期为你推送原创深度好文