Axios源碼解析

基類 Axios

跟隨入口 index.js 進入/lib/axios.js验懊,第一個方法則是createInstance創(chuàng)建Axios實例擅羞。先理解一些屬性后尸变,再看 /core/Axios.js 的代碼。

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

interceptors.request召烂,interceptors.response為InterceptorManager的實例
InterceptorManager的本質(zhì)是一個訂閱發(fā)布者模型
handlers 是收集訂閱者的容器
use 是訂閱方法,向容器中添加{ fulfilled, rejected }娃承,分別代表Promise的resolve和reject的兩種狀態(tài)
eject 是退訂方法
forEach 進行了重寫奏夫,綁定方法怕篷,遍歷通知訂閱回調(diào)函數(shù)的執(zhí)行發(fā)布

  • dispatchRequest,請求的觸發(fā)

dispatchRequest 的本質(zhì)是調(diào)用了config中的adapter方法酗昼,adapter在客戶端是返回一個Promise廊谓,內(nèi)部邏輯是對XMLHttpRequest的封裝,服務(wù)端是一個基于Node.jshttp server麻削。后面會講到 adapter蒸痹。

/core/dispatchRequest

module.exports = function dispathRequest(config) {
  // ...
  // config.adapter 返回Promise,在客戶端本質(zhì)上是對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 是用于執(zhí)行 XMLHttpRequest 中斷請求的方法abort呛哟,內(nèi)部通過高階函數(shù)實現(xiàn)叠荠,稍顯繞腦,作者的設(shè)計思路扫责,尤其是外部調(diào)用 Promise 中的 resolve 方法讓人眼前一亮榛鼎,我們放在最后講。

基類Axios /core/Axios.js

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

// axios[method]實際上就是調(diào)用的request
Axios.prototype.request = function request(config) {
  if (typeof config === 'string') {
    // 滿足axios('example/url')調(diào)用
    config = arguments[1] || {};
    config.url = arguments[0]
  } else {
    config = config || {};
  }
  // ...
  // 優(yōu)先入?yún)⒅械姆椒ū罟拢浯螢閷嵗瘯r默認的方法者娱,再次默認為 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);
  });

  // 攔截器響應(yīng)訂閱放在dispatchRequest后
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
  // 攔截器依次執(zhí)行,并修改原訂閱數(shù)組苏揣,觸發(fā)dispatchRequest肺然,執(zhí)行請求后,執(zhí)行響應(yīng)攔截器
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift())
  }
  return promise;
}

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

優(yōu)先參數(shù)設(shè)置,默認為GET方法
'delete', 'get', 'head', 'options'方法類似于get吐葱,request參數(shù)中接收method,url但不接收data
'post', 'put', 'patch'方法類似于post街望,request參數(shù)中接收method,url以及data
axios[method]實際上就是調(diào)用的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 首先會創(chuàng)建一個默認請求設(shè)置的Axios實例弟跑,默認設(shè)置中adapter屬性在客戶端指向文件/adapters/xhr灾前,導(dǎo)出一個方法都弹,即請求的發(fā)起 new XMLHttpRequest()网杆,并返回一個Promise。

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

  // 設(shè)置超時時間  
  request.timeout = confit.timeout;

  // 當request的readyState變化時炭玫,觸發(fā)
  // 0-UNSENT-代理被創(chuàng)建,但尚未調(diào)用open()方法
  // 1-OPENED-open()方法已經(jīng)被調(diào)用
  // 2-HEADERS_RECEIVED-send()方法已經(jīng)被調(diào)用貌虾,并且頭部和狀態(tài)已經(jīng)可獲得
  // 3-LOADING-下載中吞加,responseText已包含部分數(shù)據(jù)
  // 4-DONE-下載操作已完成
  request.onreadystatechange = function handleLoad() {
    // 處理已完成的請求
    if (!request || request.readyState !==4) return;
  
    // status-只讀狀態(tài)碼,請求完成前以及請求出錯,狀態(tài)碼均為0
    // responseURL-響應(yīng)的序列化URL
    // 處理已正常完成衔憨,且響應(yīng)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中可以設(shè)置屬性timeoutErrorMessage
  // 這個屬性是axios官方?jīng)]有說的,定義用于reject提供的異常message
  request.ontimeout = function...

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

  // 綁定進度函數(shù) 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狀態(tài)reject
  if (config.cancelToken) {
    config.cancelToken.promise.then(function onCanceled(cancel) {
      if (!request) return;
      request.abort();
      reject(cancel);
      request = null;
    })
  }
  // 發(fā)送請求
  request.send(requestData)
}

請求的流程

到這里码党,整個請求的流程已經(jīng)清晰了赫舒。

  1. 當執(zhí)行axios(url)或者axios[method]對應(yīng)的都是Axios中的request方法
  2. 攔截器interceptors收集訂閱,順序為闽瓢,請求攔截器接癌,dispatchRequest,響應(yīng)攔截器
  3. 攔截器Promise.then(chain.shift())執(zhí)行扣讼,首先執(zhí)行請求攔截器缺猛,并改變原訂閱數(shù)組
  4. 直至dispatchRequest觸發(fā)config.adapter(客戶端是XMLHttpRequest, 返回Promise)
  5. 后繼續(xù)Promise.then(chain.shift()),執(zhí)行響應(yīng)攔截器椭符,直至訂閱數(shù)組長度為0

在過程4荔燎,dispatchRequest觸發(fā)請求即XMLHttpRequest的執(zhí)行過程是,open初始化销钝,綁定所有方法有咨,添加屬性和配置后,send發(fā)起請求蒸健。
過程中執(zhí)行綁定的方法座享,非預(yù)期時reject;只有當readyState為4時似忧,才有可能resolve拿到我們期望的數(shù)據(jù)渣叛。
常見的使用Axios的方法總是配合著then + catchasync/await + catch使用。

CancelToken盯捌,用于中斷取消請求

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

CancelToken 源碼 /cancel/CancelToken.js

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    // executor必須是函數(shù)
    throw new TypeError('executor must be a function.');
  }
  
  // 很關(guān)鍵4狙谩!
  // promise可以在外部被調(diào)用resolve
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  // 標記1
  executor(function cancel(message) {
    // message是執(zhí)行后面source.cancel()傳入的參數(shù)
    if (token.reason) {
      // dispatchRequest 已經(jīng)從通過 config.adapter 接收到響應(yīng)結(jié)果了饺著,會調(diào)用下面的 throwIfRequested 方法
      // 無法手動終止請求
      return;
    }
    // reason 理解成一個非空字符串就好
    token.reason = new Cancel(message);
    // 很關(guān)鍵s锱省!
    // 非Promise內(nèi)部執(zhí)行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 的參數(shù) function cancel(message) 就對應(yīng)下面的參數(shù) c
  //  定義 cancel 來接收c
  // S姿ァQヵ恕!那么塑顺,cancel() 就可以調(diào)用 token.promise 中的 Promise.resolve
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    // token 中有 Promise
    token: token,
    // cancel 可以調(diào)用 token.promise 中的 Promise.resolve
    cancel: cancel
  };
};

module.exports = CancelToken;

example: 本質(zhì)上就是 cancel 執(zhí)行了 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)

// 執(zhí)行了 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源碼解析到這里就結(jié)束了扬绪,希望大家能夠看明白,能夠喜歡裤唠!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末挤牛,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子种蘸,更是在濱河造成了極大的恐慌墓赴,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,561評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件航瞭,死亡現(xiàn)場離奇詭異诫硕,居然都是意外死亡,警方通過查閱死者的電腦和手機刊侯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,218評論 3 385
  • 文/潘曉璐 我一進店門章办,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人滨彻,你說我怎么就攤上這事藕届。” “怎么了亭饵?”我有些...
    開封第一講書人閱讀 157,162評論 0 348
  • 文/不壞的土叔 我叫張陵休偶,是天一觀的道長。 經(jīng)常有香客問我辜羊,道長踏兜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,470評論 1 283
  • 正文 為了忘掉前任八秃,我火速辦了婚禮庇麦,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘喜德。我一直安慰自己山橄,他們只是感情好,可當我...
    茶點故事閱讀 65,550評論 6 385
  • 文/花漫 我一把揭開白布舍悯。 她就那樣靜靜地躺著航棱,像睡著了一般。 火紅的嫁衣襯著肌膚如雪萌衬。 梳的紋絲不亂的頭發(fā)上饮醇,一...
    開封第一講書人閱讀 49,806評論 1 290
  • 那天,我揣著相機與錄音秕豫,去河邊找鬼朴艰。 笑死观蓄,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的祠墅。 我是一名探鬼主播侮穿,決...
    沈念sama閱讀 38,951評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼毁嗦!你這毒婦竟也來了亲茅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,712評論 0 266
  • 序言:老撾萬榮一對情侶失蹤狗准,失蹤者是張志新(化名)和其女友劉穎克锣,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腔长,經(jīng)...
    沈念sama閱讀 44,166評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡袭祟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,510評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了捞附。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巾乳。...
    茶點故事閱讀 38,643評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖故俐,靈堂內(nèi)的尸體忽然破棺而出想鹰,到底是詐尸還是另有隱情,我是刑警寧澤药版,帶...
    沈念sama閱讀 34,306評論 4 330
  • 正文 年R本政府宣布辑舷,位于F島的核電站,受9級特大地震影響槽片,放射性物質(zhì)發(fā)生泄漏何缓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,930評論 3 313
  • 文/蒙蒙 一还栓、第九天 我趴在偏房一處隱蔽的房頂上張望碌廓。 院中可真熱鬧,春花似錦剩盒、人聲如沸谷婆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,745評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纪挎。三九已至,卻和暖如春跟匆,著一層夾襖步出監(jiān)牢的瞬間异袄,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,983評論 1 266
  • 我被黑心中介騙來泰國打工玛臂, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烤蜕,地道東北人封孙。 一個月前我還...
    沈念sama閱讀 46,351評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像讽营,于是被迫代替她去往敵國和親虎忌。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,509評論 2 348