Axios源碼解析

基類 Axios

跟隨入口 index.js 進入/lib/axios.js,第一個方法則是createInstance創建Axios實例。先理解一些屬性后,再看 /core/Axios.js 的代碼。

  • interceptors,攔截器 /core/InterceptorManager.js

interceptors.request,interceptors.response為InterceptorManager的實例
InterceptorManager的本質是一個訂閱發布者模型
handlers 是收集訂閱者的容器
use 是訂閱方法,向容器中添加{ fulfilled, rejected },分別代表Promise的resolve和reject的兩種狀態
eject 是退訂方法
forEach 進行了重寫,綁定方法,遍歷通知訂閱回調函數的執行發布

  • dispatchRequest,請求的觸發

dispatchRequest 的本質是調用了config中的adapter方法,adapter在客戶端是返回一個Promise,內部邏輯是對XMLHttpRequest的封裝,服務端是一個基于Node.jshttp server。后面會講到 adapter

/core/dispatchRequest

module.exports = function dispathRequest(config) {
  // ...
  // config.adapter 返回Promise,在客戶端本質上是對XMLHttpRequest的封裝
 var adapter = config.adapter || defaults.adapter;
  return adapter(config).then(function onAdapterResolution(response) {
    // ...
    return response;
  }, function onAdapterRejection(reason) {
    // ...
    return Promise.reject(reason)
  })
}
  • cancelToken 取消請求的令牌

cancelToken 是用于執行 XMLHttpRequest 中斷請求的方法abort,內部通過高階函數實現,稍顯繞腦,作者的設計思路,尤其是外部調用 Promise 中的 resolve 方法讓人眼前一亮,我們放在最后講。

基類Axios /core/Axios.js

function Axios(instanceConfig) {
  // 緩存請求設置
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  }
}

// axios[method]實際上就是調用的request
Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    // 滿足axios('example/url')調用
    config = arguments[1] || {};
    config.url = arguments[0]
  } else {
    config = config || {};
  }
  // ...
  // 優先入參中的方法,其次為實例化時默認的方法,再次默認為 GET請求
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  // 攔截器請求訂閱放在dispatchRequest前
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  // 攔截器響應訂閱放在dispatchRequest后
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  // 攔截器依次執行,并修改原訂閱數組,觸發dispatchRequest,執行請求后,執行響應攔截器
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift())
  }
  return promise;
}

// 返回請求路徑,處理了get請求的queryString拼接
Axios.prototype.getUri = function (...) {
  // ...
}
// ...
module.exports = Axios;
  • methods,請求方法

優先參數設置,默認為GET方法
'delete', 'get', 'head', 'options'方法類似于get,request參數中接收method,url但不接收data
'post', 'put', 'patch'方法類似于post,request參數中接收method,url以及data
axios[method]實際上就是調用的request({ method, url, ... })

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
    }));
  };
});

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
    }));
  };
});

defaultConfig adapter - xhrAdapter,客戶端XMLHttpRequest

/lib/axios.js 首先會創建一個默認請求設置的Axios實例,默認設置中adapter屬性在客戶端指向文件/adapters/xhr,導出一個方法,即請求的發起 new XMLHttpRequest(),并返回一個Promise。

module.exports = function xhrAdapter(config) {
  // ...
  if (utils.isFormData(requestData)) {
    // 如果提交的是form表單,則要瀏覽器去設置Content-Type,"multipart/form-data"
    delete requestHeaders['Content-Type'];
  }
  // 實例化XMLHttpRequest對象
  var request = new XMLHttpRequest();
  
  if (config.auth) {
    // ...
    // 設置 Authorization 頭信息
    requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
  }
  // ...
  // 初始化一個異步請求
  request.open(method, url, true)

  // 設置超時時間  
  request.timeout = confit.timeout;

  // 當request的readyState變化時,觸發
  // 0-UNSENT-代理被創建,但尚未調用open()方法
  // 1-OPENED-open()方法已經被調用
  // 2-HEADERS_RECEIVED-send()方法已經被調用,并且頭部和狀態已經可獲得
  // 3-LOADING-下載中,responseText已包含部分數據
  // 4-DONE-下載操作已完成
  request.onreadystatechange = function handleLoad() {
    // 處理已完成的請求
    if (!request || request.readyState !==4) return;
  
    // status-只讀狀態碼,請求完成前以及請求出錯,狀態碼均為0
    // responseURL-響應的序列化URL
    // 處理已正常完成,且響應URL為非文件的請求
    if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) return;
    var response = {
      data: requset.responseType === 'text' ? requset.responseText : request.response,
      status: request.status,
      statusText: request.statusText,
      headers: parseHeaders(request.getAllResponseHeaders()),
      config: config,
      request: request
    }
    resolve(response);
    request = null;
  }

  // 請求終止
  request.onabort = function ...
  
  // 請求異常
  request.onerror = function ...

  // 請求超時,config中可以設置屬性timeoutErrorMessage
  // 這個屬性是axios官方沒有說的,定義用于reject提供的異常message
  request.ontimeout = function...

  // 配置XMLHttpRequest頭信息屬性 responseType, withCredentials等...

  // 綁定進度函數 config.onDownloadProgress config.onUploadProgress
  if (typeof config.onDownloadProgress === 'function') {
    request.addEventListener('progress', config.onDownloadProgress);
  }
  // 上傳進度還需要判斷瀏覽器是否支持,loadstart, loadend, progress等進度都需要綁定在upload上
  if (typeof config.onUploadProgress === 'function' && request.upload) {
    request.upload.addEventListener('progress', config.onUploadProgress)
  }
  // 取消令牌,終止請求,Promise狀態reject
  if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
      if (!request) return;
      request.abort();
      reject(cancel);
      request = null;
    })
  }
  // 發送請求
  request.send(requestData)
}

請求的流程

到這里,整個請求的流程已經清晰了。

  1. 當執行axios(url)或者axios[method]對應的都是Axios中的request方法
  2. 攔截器interceptors收集訂閱,順序為,請求攔截器,dispatchRequest,響應攔截器
  3. 攔截器Promise.then(chain.shift())執行,首先執行請求攔截器,并改變原訂閱數組
  4. 直至dispatchRequest觸發config.adapter(客戶端是XMLHttpRequest, 返回Promise)
  5. 后繼續Promise.then(chain.shift()),執行響應攔截器,直至訂閱數組長度為0

在過程4,dispatchRequest觸發請求即XMLHttpRequest的執行過程是,open初始化,綁定所有方法,添加屬性和配置后,send發起請求。
過程中執行綁定的方法,非預期時reject;只有當readyState為4時,才有可能resolve拿到我們期望的數據。
常見的使用Axios的方法總是配合著then + catchasync/await + catch使用。

CancelToken,用于中斷取消請求

首先對比下CancelToken的源碼與CancelToken的使用方式

CancelToken 源碼 /cancel/CancelToken.js

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    // executor必須是函數
    throw new TypeError('executor must be a function.');
  }
  
  // 很關鍵??!
  // promise可以在外部被調用resolve
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  // 標記1
  executor(function cancel(message) {
    // message是執行后面source.cancel()傳入的參數
    if (token.reason) {
      // dispatchRequest 已經從通過 config.adapter 接收到響應結果了,會調用下面的 throwIfRequested 方法
      // 無法手動終止請求
      return;
    }
    // reason 理解成一個非空字符串就好
    token.reason = new Cancel(message);
    // 很關鍵!!
    // 非Promise內部執行CancelToken.promise的Promise.resolve(token.reason)
    resolvePromise(token.reason);
  });
}

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

CancelToken.source = function source() {
  var cancel;
  // 定義 token 接收一個 CancelToken 實例
  // 上文的標記1中的 executor 的參數 function cancel(message) 就對應下面的參數 c
  //  定義 cancel 來接收c
  // ?。。∧敲矗琧ancel() 就可以調用 token.promise 中的 Promise.resolve
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    // token 中有 Promise
    token: token,
    // cancel 可以調用 token.promise 中的 Promise.resolve
    cancel: cancel
  };
};

module.exports = CancelToken;

example: 本質上就是 cancel 執行了 token.promise 中的 Promise.resolve

const CancelToken = axios.CancelToken;
const source = CancelToken.source();
 
axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});
 
axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})
 
// cancel the request (the message parameter is optional)

// 執行了 config.cancelToken.promise 中的 Promise.resolve
source.cancel('手動中斷請求'');

Promise.resolve那么然后呢?還記得最初提到的XMLHttpRequest的abort方法嗎?

xhr /adapters/xhr.js

// ...
// 都清晰了吧
if (config.cancelToken) {
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) return;
    request.abort();
    reject(cancel);
    request = null;
  })
}

// ...

好啦!Axios源碼解析到這里就結束了,希望大家能夠看明白,能夠喜歡!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373