前端看源码,就从axios开始吧

前端要看源码,就从axios开始吧,由于它的逻辑不只没有很复杂,并且设计也很巧妙。打包后只有一千六百多行,挑三拣四,蜻蜓点水,去掉注释后,就更没多少了。
来句官方的话:Axios 是一个基于 promise 的 HTTP 库,能够用在浏览器和 node.js 中。javascript

一、axios的用法

1-一、第一种:

在发起[delete, get, head, options]请求时,能够这样使用:前端

axios.get('/user',{
    headers,
    data
}).then(data=>{
    //do something
})
复制代码

在发起[post, put, patch]请求时,能够这样使用:java

axios.post('/user',data,{
    headers,
}).then(data=>{
    //do something
})
复制代码

这种方式发起请求,能够看到,axios被看成了一个对象,该对象上拥有许多方法,看起来就是这样子的:node

let axios = {
    delete() { },
    get() { },
    head() { },
    options() { },
    post() { },
    put() { },
    patch() { }
}
复制代码

1-二、第二种:

发送全部类型请求时,均可以这样使用:ios

axios('/user',{
    method:'get',
    headers,
}).then(data=>{
    //do something
})
复制代码

或者json

axios({
    url:'/user',
    method:'get',
    headers,
}).then(data=>{
    //do something
})
复制代码

固然,get请求能够简化成这样:axios

axios('/user').then(data=>{
    //do something
})
复制代码

这种方式发起请求,能够看到,axios被看成了一个函数,看起来是这样子的:promise

function axios(url,config) {
    //do something
}
复制代码

二、axios是什么

2-一、axios既是对象又是方法

经过了解axios的用法,咱们知道了,axios既能够是一个对象,也能够是一个函数,看成为对象时,它身上挂载了不少请求方法,好比:getposthead等等;看成为函数时,它能够直接调用,传递配置参数,参数传递有两种形式,分别是axios(url[,config])axios(config)。要作到这种效果,咱们先来了解一下函数。浏览器

2-二、函数的3种角色:

Javascript中函数是很值得思考的东西,好比说,它有3种角色:app

2-2-一、第一种:普通函数

函数就是一个普通函数,这种角色在常见不过了,也是咱们常用的,好比定义一个函数foo,而后调用它,能够是下面这样:

//定义
function foo(){
    //do something
}
//调用
foo()
复制代码

2-2-二、第二种:构造函数

函数也能够被看成一个构造函数来使用,好比定义一个构造函数Foo,而后获取它的一个实例,能够是下面这样:

//定义 
function Foo(){
    //do something
}
//获取一个实例
let instance=new Foo()
复制代码

这里函数名首字母大写纯粹是为了向某语言、某标准、某约定俗成看齐,其余什么做用都没有(无知的说这话)

2-2-三、第三种:对象

敲黑板,划重点,函数也能够被看成一个对象,也就是说,咱们能够将一个函数做为对象来使用,给它定义属性,获取它的属性值,此时和它的另一个身份(函数)毫无关系。好比有一个函数foo,我就不把你当成函数,就把它当成对象,给它定义属性,获取它的属性值,ok,没毛病。

function foo(){
    //do something
}

//给foo定义一个属性,值为数字
foo.a=1;

//给foo定义一个属性,值为函数
foo.b=function(){};

//获取foo的属性a的值
console.log(foo.a)
复制代码

如今,我又想把foo当成一个函数对待了,ok,没毛病,你是导演,你说了算。

//如今把foo当成函数来调用
foo()
复制代码

了解了这些,咱们来看下axios是如何作到多种使用方式的。

2-三、axios源码中是如何作到多种使用方式的

源码以下:

function createInstance(defaultConfig) {
   //获取Axios的一个实例
  var context = new Axios(defaultConfig);
  //将Axios原型上的request方法的上下文(也就是this)绑定为刚建立的实例
  var instance = bind(Axios.prototype.request, context);
  //将Axios原型上的属性和方法扩展到instance上
  utils.extend(instance, Axios.prototype, context);
  //将建立的实例上的属性和方法扩展到instance上
  utils.extend(instance, context);
  //返回instance
   return instance;
}
//axios就是上面的instance
var axios = createInstance(defaults);

//...

module.exports = axios;
复制代码

上面代码中,Axios.prototype.request是一个方法,用工具函数bind将它的上下文(this)绑定为Axios的实例,获得instance,也就是说instance是绑定this后的request方法,接下来就是围绕instance来搞事情了。 instance自己是一个函数,可是这里,将它做为一个对象来处理了,给它身上定义了一些属性和方法。

Axios.prototype上的属性和方法扩展到了instance上,Axios.prototype上有哪些属性和方法呢,源码以下:

//定义request方法,该方法是重点,其余调用方式最终调用的都是这个方法
Axios.prototype.request = function request(config){/* ... */}
//在原型上批量定义方法,这些方法不接收请求体数据,好比get请求
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {/* ... */};
});
//在原型上批量定义方法,这些方法接收请求体数据,好比post请求
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {/* ... */};
});
复制代码

上面代码中都是在Axios的原型上定义方法,首先定义request方法,其次批量定义[delete, get, head, options]方法,最后批量定义[post, put, patch]方法,也就是说给原型上定义的这些方法最后都会被扩展到instance上,咱们就能够这样使用了:axios.get()axios.post()等等。 其实,不管咱们是将axios做为一个对象来调用getpost方法,仍是将axios做为一个函数来调用,最终调用的都是原型上的request方法。

三、config的合并顺序

了解了axios是如何作到多种使用方式的,接下来看一下axios中的配置,也就是用户传入的配置项是如何走完整个流程的。

3-一、经过axios.get()等没有请求体方法传入的配置

当咱们这样调用axios时:

axios.get('/user',{headers,timeout,...})
复制代码

axios源码内部会将咱们传入的配置作一层处理,这层处理很简单,首先判断是否传递config,而后将methodurl合并到config,最后调用request方法,并将处理后的配置传递给request。 源码以下:

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});
复制代码

3-二、经过axios.post()等有请求体方法传入的配置

当咱们这样调用axios时:

axios.post('/user',{name:'wz'},{headers,timeout,...})
复制代码

axios内部的处理方式和上面调用get方式几乎相同,惟一不一样的是多处理了一下请求体数据。源码以下:

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data //处理请求体参数
    }));
  };
});
复制代码

3-三、Axios.prototype.request方法中对配置作了什么

axios.get()axios.postaxios(),不管是哪一种调用方式,最终都会调用request方法,看一下关于request方法的源码:

Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    config = utils.merge({
      url: arguments[0]
    }, arguments[1]);
  }

  //...

  //这里能够看出配置的优先顺序,从左至右依次增高
  config = utils.merge(defaults, {method: 'get'}, this.defaults, config);
  //将请求方法变为小写
  config.method = config.method.toLowerCase();

  //...
};
复制代码

request方法先对参数作一次判断,若是第一个参数类型为字符串,也就是咱们是这样使用的,axios('/user',{...}),会将字符串放到一个新对象上的url属性上,而后和第二个参数作依次合并。接下来就是按照从小到大的权重合并多个配置。

  1. defaultsaxios的默认配置,权重最低。
  2. {method:'get'}是设置默认请求方法。
  3. this.defaults,是新建立axios实例时传入的配置对象.axios.create({...})
  4. config,调用axios时传递的参数,权重最高。

到这里axios内部对config的处理就算完成了,接下来,config会被依次交给请求拦截器去处理,让咱们看一下axios中的拦截器吧。

四、组织拦截器、转换器、适配器

先看一下这3种东西的关系,以下图:


这是三者的流程关系,拦截器和转换器能够有多个,他们的职责不同
请求拦截器的任务是处理请求配置
转换器是处理请求体数据
适配器的做用是根据环境来肯定使用哪一种请求方法,浏览器环境使用 XMLHttpRequestnode环境使用 http(s).request()方法;响应拦截器是处理响应数据。

4-一、拦截器

拦截器是一个函数,分为两种类型:请求拦截器响应拦截器,请求拦截器的主要做用是,提供了一种机制,使得咱们能够集中处理各类请求时传递的参数,就比如人们去乘坐地铁同样,无论你是从哪里来的,手里拿着什么东西,只要乘坐地铁都会通过安检,拦截器就比如这道安检同样,每一个安检有各自的安检任务,第一个安检经过后,会转交到第二个安检,第三个,直到全部的安检都经过了,才能抵达候车区。

axios中关于拦截器的源码:

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);//将遍历到的每个拦截器回传给外部
    }
  });
};
复制代码

拦截器相关的源码不难理解,InterceptorManager构造函数原型上定义了拦截器的添加、删除、遍历3个方法。须要注意的是,每个拦截器又分为两个方法,成功和失败,这是由于,axios是基于promise实现链式调用的,因此每个拦截器的成功失败方法会分别传递给promisethen方法当中。对promise不太了解的,能够看这里

响应拦截器的道理和请求拦截器同样,只不过响应拦截器拦截的是返回的数据,而请求拦截器拦截的是请求的配置。 接下来就走到转换器了。

4-二、转换器

转换器是一个函数,分为两种类型:请求数据转换器和响应数据转换器。请求转换器函数接受2个参数,分别为data,headers,主要是处理请求体和请求头的。响应数据转换器主要是处理响应数据的,好比将JSON数据转为普通数据。
源码中默认的转换器:

let defaults={
    //请求数据转化器
    transformRequest: [function transformRequest(data, headers) {
        normalizeHeaderName(headers, 'Content-Type');
        //判断请求体数据类型为如下任一一种的话,直接返回
        if (utils.isFormData(data) ||
            utils.isArrayBuffer(data) ||
            utils.isBuffer(data) ||
            utils.isStream(data) ||
            utils.isFile(data) ||
            utils.isBlob(data)
        ) {
            return data;
        }
        if (utils.isArrayBufferView(data)) {
            return data.buffer;
        }
        if (utils.isURLSearchParams(data)) {
            setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
            return data.toString();
        }
        if (utils.isObject(data)) {
            setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
            return JSON.stringify(data);
        }
        return data;
    }],
    //响应数据转换器
    transformResponse: [function transformResponse(data) {
        if (typeof data === 'string') {
            try {
                data = JSON.parse(data);
            } catch (e) { /* Ignore */ }
        }
        return data;
    }],
}
复制代码

上面代码简单看看就能够了,大概知道拦截器在作什么事情,其实仔细思考能够发现,转化器作到事情在拦截器里一样可以作到,只不过为了职责单一,分工明确,将数据处理这部分工做单独放到转换器里来作了。
接下来就到发送请求了,这部分工做是由适配器作的。

4-三、适配器

适配器会根据当前使用axios的环境来决定使用哪一种工具去发送请求,当前环境是浏览器的话,就会使用XMLHttpRequest,是node的话,会使用http(s).request()
源码以下:

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // 浏览器环境,使用 xhr 适配器
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined') {
    // node环境,使用 http 适配器
    adapter = require('./adapters/http');
  }
  return adapter;
}
复制代码

./adapters/xhr文件里,除了对IE进行一些判断处理外,基本上就是发送AJAX那几个经典步骤了,以下:

//获取 XMLHttpRequest 实例
let xhr = new new XMLHttpRequest();
//肯定请求方法等参数
xhr.open(method,url,...);
//监听请求状态
xhr.onreadystatechange = function () {
    //...
}
//发送请求
xhr.send();
复制代码

./adapters/http文件里,是使用node的包httphttps来完成请求的。

4-四、如何链式调用拦截器、转换器、适配器

axios内部是如何组织拦截器、转换器、适配器呢,看一下源码:

Axios.prototype.request = function request(config) {
  //chain里存放着全部的拦截器、转换器、适配器
  var chain = [dispatchRequest, undefined];
  //生成一个成功的promise,数据为通过一系列处理后的配置
  var promise = Promise.resolve(config);
  //this.interceptors.request存放着全部的请求拦截器
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    //将全部的请求拦截器依次放入chain容器中,这里使用的是unshift,
    //也就是说,请求拦截器被是从chain容器的头部开始,依次放入。
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  //this.interceptors.response存放着全部的响应拦截器
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    //将全部的响应拦截器依次放入chain容器中,这里使用的是push,
    //也就是说,响应拦截器被是从chain容器的尾部开始,依次放入。
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    //链式调用chain容器中的拦截器、转换器、适配器,注意这里使用的是
    //chain.shift()方法,就是说取的时候是从chain容器头部开始,直到尾部,线性顺序。
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};
复制代码

chain容器在初始化的时候就加入了dispatchRequestundefined,接下来咱们看一下dispatchRequest具体作了什么

五、dispatchRequest都作了什么

从发送一个请求到这里只剩下这几件事情了,

  1. 请求数据转换器
  2. 请求适配器
  3. 响应数据转换器 dispatchRequest主要就是作了以上3件事。 简化后的源码以下:
module.exports = function dispatchRequest(config) {
  // 转化请求数据
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );
  var adapter = config.adapter || defaults.adapter;
  //adapter方法会根据调用请求方法所在的环境来选择具体的请求方法。
  return adapter(config).then(function onAdapterResolution(response) {
    // 转换响应数据
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;
  }, function onAdapterRejection(reason) {/* ... */});
};
复制代码

六、整体流程

回顾一下,从发送一个请求开始,到成功接收到响应,这风风雨雨的一路,全在图里了。

相关文章
相关标签/搜索