前端看源碼,就從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) {/* ... */});
};
複製代碼

六、整體流程

回顧一下,從發送一個請求開始,到成功接收到響應,這風風雨雨的一路,全在圖裏了。

相關文章
相關標籤/搜索