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
  // !!!那么,cancel() 就可以调用 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阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349