一比一還原axios原始碼(四)—— Axios類

Zaking發表於2022-03-17

  axios原始碼的分析,到目前為止,算上第0章已經四章了,但是實際上,還都沒有進入axios真正的主線,我們來簡單回顧下。最開始我們構建了get請求,寫了重要的buildURL方法,然後我們處理請求體請求頭,響應體響應頭,這樣我們就可以傳json物件了,然後還加入了promise,讓我們可以鏈式點用,最後還加了錯誤處理,讓我們可以更好的操作請求資訊。

  但是,大家發現了沒有,目前為止我們所寫的核心其實就是一個XMLHttpRequest物件,所有的內容都圍繞著這個物件。程式碼也沒有做太清晰的分割,那麼今天,我們就來完成axios的核心主題,也就是Axios類,有了這個,大家就可以通過一些直觀的方法來快速的呼叫axios的請求API了。

  依照慣例,從axios的API入手,我們今天要實現的內容如下:

 

  那麼接下來我們就進入正題吧。

  首先,我們在core資料夾下建立一個Axios檔案。宣告一個Axios類:

export default function Axios(config) {}

  這個axios很簡單,我們暫時這樣,什麼都不需要。然後我們在Axios的原型上掛載一個request方法,這個方法是真正的請求方法,也就是說,所有的axios請求,其實都是request。

Axios.prototype.request = function (url, config) {
  if (typeof url === "string") {
    if (!config) {
      config = {};
    }
    config.url = url;
  } else {
    config = url;
  }
  return dispatchRequest(config);
};

  首先我們來看下,request方法實際上有兩個核心,一個是引數的過載,聽起來很高大上,實際上就是可以傳一個引數,也可以把url單獨抽離出來作為引數及其他的config來傳遞,最重要的就是dispatchRequest,上面說所有的axios的請求都是request,那麼其實request,就是dispatchRequest。我們來看下,怎麼搞出來的dispatchRequest。

  

   還記不記得之前lib根目錄下的axios,沒錯,把裡面的程式碼複製過來就可以了。那axios就沒東西了,我們改下axios裡的程式碼:

import Axios from "./core/Axios";
import bind from "./helpers/bind";
import utils from "./utils";
/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 * @return {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

// Create the default instance to be exported
var axios = createInstance();

// Expose Axios class to allow class inheritance
axios.Axios = Axios;

export default axios;

  誒?怎麼程式碼風格變了?好吧,我承認這是從axios原始碼複製過來的,毛都沒改,就改了改引用。然後呢,這個createInstance實際上就是個工廠函式。建立並返回axios的例項。我們暫時不看extend和bind具體的原始碼,從字面意思來看,instance例項上繫結request方法,也就是說,我可以直接使用axios.request。extend就是把某些東西,也就是複製了屬性到例項上。OK,到此,核心的axios體系基本上完成了。但是我們還漏了一個很重要的事情,就是本章最開始的呼叫方式,我們希望可以在例項上直接呼叫get、post等方法。那麼我們來看下程式碼:

// Provide aliases for supported request methods
utils.forEach(
  ["delete", "get", "head", "options"],
  function forEachMethodNoData(method) {
    /*eslint func-names:0*/
    Axios.prototype[method] = function (url, config) {
      return this.request(
        mergeConfig(config || {}, {
          method: method,
          url: url,
          data: (config || {}).data,
        })
      );
    };
  }
);

utils.forEach(["post", "put", "patch"], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function (url, data, config) {
    return this.request(
      mergeConfig(config || {}, {
        method: method,
        url: url,
        data: data,
      })
    );
  };
});

  上面把方法分成了兩類,一類是有body的,一類是沒有的。然後呼叫request,把引數傳進去就好了,簡單的一批。至於mergeConfig方法,我們們稍後面再說。我會盡可能的把他們都註釋一遍,可以去原始碼裡查閱,因為這些東西都差不多可以拆出來,單獨使用,不在axios的核心線上,utils是單純的工具,與業務無關,而helpers包含了對業務的一定的抽象和關聯。到這裡,我們就可以使用axiso.get這樣的方法來呼叫介面了。

  那,額外的,我們來分析下bind、extend和mergeConfig方法:

1.bind

  我們先來看下程式碼:

export default function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
}

  我們們先來字面看一下,傳入了一個fn和thisArg引數,然後返回了一個wrap函式。wrap裡面根據wrap的arguments長度建立了個陣列,然後挨個的把arguments的引數複製給args,然後再返回一個fn.apply。所以,字面意思我們們理解了,但是它到底幹了啥呢?我們就拿用到了它的地方做個舉例:

  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  那怎們把引數傳進去,其實他就是:

return Axios.prototype.request.apply(context, args);

  解釋下這句話吧,就是request方法的this只想context,也就是new Axios(defaultConfig),然後把args作為引數傳進去,那args就是傳給wrap的引數。那我們再寫個例子:

<!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></body>
  <script>
    function bind(fn, thisArg) {
      return function wrap() {
        console.log(arguments);
        var args = new Array(arguments.length);
        for (var i = 0; i < args.length; i++) {
          args[i] = arguments[i];
        }
        return fn.apply(thisArg, args);
      };
    }

    function A(config) {
      console.log(config);
    }
    A.prototype.request = function () {
      console.log("request");
    };
    var context = new A({ a: 2, b: 3 });
    var instance = bind(A.prototype.request, context);
    instance(1, 2, 3);
  </script>
</html>

  大家捋一下哦。我們再回到這塊程式碼:

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);

  // Copy axios.prototype to instance
  utils.extend(instance, Axios.prototype, context);

  // Copy context to instance
  utils.extend(instance, context);

  return instance;
}

  最後,我們返回了instance,對吧?那麼就意味著我們可以這樣用:

instance({
   method:'post',
   // xxxxxx  
})

  那我有個問題,最終這個傳遞的引數給了誰?如果不知道答案的話,那就再回頭看一遍吧~~~。

2.extend

  extend方法,說白了,就是把a的屬性,複製給b沒了。我們來看下程式碼:

function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === "function") {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

  我們來看下,字面意思的話,就是,如果有thisArg,並且某一個b中的屬性是一個函式,那麼a中對應的key就是bind後的函式,否則就是單純的複製。簡單吧~~~。那這裡我就不說bind是咋回事了啊,看不懂回頭看啊。我們們這叫一步三回頭。:)

3.mergeConfig

  這個呢,看起來複雜, 說起來簡單,因為篇幅較長,我就不在這裡說了,大家自己去專案中對應的分支看註釋哦。但是我簡單說下,這個mergeConfig實際上使用了一種策略模式,簡單點說其實就是根據不同的物件,來分配不同的合併方法。一共有那麼1、2、3、4、5,哦對,四種合併策略(去看了原始碼你就知道我這裡沒說錯了,我擴起來說是怕你罵我,你罵我倒無所謂,我怕你罵錯了,嘻嘻)。

  我們再來回顧下,今天的核心主線:

  1. 建立Axios類。
  2. 在Axios的原型上擴充套件核心request方法。
  3. 擴充套件其他alias方法,內部就是呼叫的request。
  4. 建立dispatchRequest,是request方法的核心(就是之前舊的lib/axios裡的程式碼)。
  5. 建立createInstance工廠函式,繫結資料到例項上。
  6. 解釋了bind、extend方法的含義,mergeConfig自己去程式碼看。

  今天就做了這些,其實不復雜,跟著我,帶你一比一還原axios(其實就是教你怎麼抄)。額……咳咳……讀書人的事,怎麼能叫做抄呢~~我們下一章子再抄,哦不,再借鑑噢。

相關文章