Axios源码阅读(一):核心功能源码

一、Axios核心功能梳理

先看下Axios官方文档的介绍:

Axios is a promise-based HTTP Client for node.js and the browser. It is isomorphic (= it can run in the browser and nodejs with the same codebase). On the server-side it uses the native node.js http module, while on the client (browser) it uses XMLHttpRequests.

通过介绍我们了解到Axios是一个可以运行在浏览器和Node.js环境的基于PromiseHTTP库,那么既然是一个HTTP库,核心功能自然是构造并发起HTTP请求。那么我们就从Axios如何实现一个GET/POST请求为切入点进行阅读。

二、源码阅读

引用文档上第一个示例代码:

const axios = require('axios');

// Make a request for a user with a given ID
axios.get('/user?ID=12345')
  .then(function (response) {
    // handle success
    console.log(response);
  })
  .catch(function (error) {
    // handle error
    console.log(error);
  })
  .then(function () {
    // always executed
  });

通过以上代码我们了解到:

  1. Axios库暴露出的axios上有一个get方法;
  2. 使用get方法可以发起HTTP请求;
  3. get方法返回一个promise对象;

下面我们围绕这三点展开阅读。

2.1 axios 对象和 get 方法

首先通过入口文件index.js找到暴露对象来自./lib/axios.js,打开该文件找到了module.exports.default = axios这行代码,所以这个文件的axios对象就是示例代码中使用的对象,我们看下这个对象是如何产生的。

传送门:./lib/axios.js

var Axios = require('./core/Axios');

/**
 * 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(defaults);

传送门:./lib/core/Axios.js

/**
 * Create a new instance of Axios
 *
 * @param {Object} instanceConfig The default config for the instance
 */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

我们整理下上面代码的逻辑:

  1. 使用库提供的默认配置defaults通过Axios构造函数生成一个上下文对象context
  2. 声明变量instanceAxios.prototype.request并且绑定thiscontext
  3. instance上扩展了Axios.prototype的属性以及defaultsinterceptors属性;
  4. 返回instance,也就是说Axios库暴露的对象就是这个的instantce对象。

总结一下axios的本质是Axios.prototype.request,并且扩展了Axios.prototype的属性和方法。那么axios.get也就是Axios.prototype上的方法咯,所以我们再次打开Axios.js文件一探究竟。

传送门:.lib/core/Axios.js

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

从上面代码可以看出,axios上的getpostput等方法都是Axios.prototype.request的别名,所以使用Axios发出的HTTP请求其实都是Axios.prototype.request发起的。这也就解释了为什么发起一个get请求可以有2种写法:

  • axios.get(url)
  • axios({method: 'get', url: url})

2.2 axios.get 发起请求的过程

通过前面的代码,我们已经知道axios所有的请求都是通过Axios.prototype.request发起的,所以我们找到这个方法:

传送门:./lib/core/Axios.js

/**
 * Dispatch a request
 *
 * @param {Object} config The config specific for this request (merged with this.defaults)
 */
Axios.prototype.request = function request(config) {
  /*eslint no-param-reassign:0*/
  // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  config = mergeConfig(this.defaults, config);

  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  
  // 注意:这里我删去了与主流程无关的代码
  // 删去的代码主要功能:
  // 1. 判断拦截器是异步调用还是同步调用
  // 2. 把拦截器的 fulfilled回调 和 rejected回调整理到数组中

  var promise;

  if (!synchronousRequestInterceptors) {
    // 如果拦截器异步调用
    var chain = [dispatchRequest, undefined];
    
    Array.prototype.unshift.apply(chain, requestInterceptorChain);
    chain.concat(responseInterceptorChain);
    
    promise = Promise.resolve(config);
    while (chain.length) {
      promise = promise.then(chain.shift(), chain.shift());
    }
    
    // 返回一个从请求拦截器开始的链式调用的 promise 对象
    return promise;
  }

  
  // 如果拦截器是同步调用的
  var newConfig = config;
  while (requestInterceptorChain.length) {
    var onFulfilled = requestInterceptorChain.shift();
    var onRejected = requestInterceptorChain.shift();
    try {
      newConfig = onFulfilled(newConfig);
    } catch (error) {
      onRejected(error);
      break;
    }
  }

  try {
    promise = dispatchRequest(newConfig);
  } catch (error) {
    return Promise.reject(error);
  }

  while (responseInterceptorChain.length) {
    promise = promise.then(responseInterceptorChain.shift(), responseInterceptorChain.shift());
  }
  
  // 返回一个从 dispatchRequest 开始的链式调用的 promise 对象
  return promise;
};

从上面代码我们了解到:在reqeust方法中使用promise对整个处理过程进行了封装,在没有拦截器的情况下返回的是dispatchRequest(newConfig),也就是说在dispatchRequest中发起请求并返回一个promise,我们找到dispatchRequest的代码:

传送门: ./lib/core/dispatchRequest.js

module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // 删除了对 headers 和对 data 的处理,查看源代码点击上方传送门

  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
    // 对响应的处理
    return response;
  }, function onAdapterRejection(reason) {
    // 请求失败后对响应的处理
    return Promise.reject(reason);
  });
};

dispatchRequest返回的是adapter的结果,adapter来自config,而axios是通过默认配置产生的,所以我们找到defaults.js的代码:

传送门: ./lib/defaults.js

var defaults = {
  adapter: getDefaultAdapter(),
  // 删除无关的属性
}

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

这里adapter是一个适配器,因为axios在浏览器时依赖XHR对象,在node环境运行时依赖底层的http库,这两个对象都不是基于promise的,但是Axios希望和他们用promise交互,所以这里需要适配器来做一个兼容,为交互双方提供桥梁。我们找到浏览器环境下的xhr.js代码:

传送门: ./lib/adapters/xhr.js

module.exports = function xhrAdapter(config) {
  // 在这里使用 axios 对 xhr 进行封装
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    // 这里我去掉了与认证以及请求头相关的处理
    
    // 实例化 xhr 对象
    var request = new XMLHttpRequest();
    // 构造 url
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // Set the request timeout in MS
    // 设置超时时间
    request.timeout = config.timeout;
    
    // loadend回调
    function onloadend() {
      if (!request) {
        return;
      }
      // Prepare the response
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      var responseData = !responseType || responseType === 'text' ||  responseType === 'json' ?
        request.responseText : request.response;
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };

      settle(resolve, reject, response);

      // Clean up request
      request = null;
    }

    if ('onloadend' in request) {
      // Use onloadend if available
      request.onloadend = onloadend;
    } else {
      // Listen for ready state to emulate onloadend
      request.onreadystatechange = function handleLoad() {
        if (!request || request.readyState !== 4) {
          return;
        }

        // The request errored out and we didn't get a response, this will be
        // handled by onerror instead
        // With one exception: request that using file: protocol, most browsers
        // will return status as 0 even though it's a successful request
        // 这里看注释是为了处理当使用文件传输协议的时候浏览器会返回状态码 0
        if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
          return;
        }
        // readystate handler is calling before onerror or ontimeout handlers,
        // so we should call onloadend on the next 'tick'
        setTimeout(onloadend);
      };
    }

    // 这里删除了防范xsrf,以及一些其他的 eventhandler
    request.onabort = function handleAbort() {
      // do sth ...
      request = null;
    };

    // Handle low level network errors
    request.onerror = function handleError() {
      // do sth ...
      request = null;
    };

    // Handle timeout
    request.ontimeout = function handleTimeout() {
      // do sth ...
      request = null;
    };
    
    // Send the request
    request.send(requestData);
  });
};

上面的代码直接印证了文档所说的Axios在浏览器是基于XHR对象的,并且在这里清晰的展示了如何用promisexhr进行封装。

总结下axios发起请求的过程:

  1. Axios.prototype.request方法中针对请求拦截器的同步/异步返回不同的promise(分别是拦截器的promisedispatchRequestpromise);
  2. dispatchReques中对请求头和请求的实体做了处理,然后返回adapter执行结果的promise
  3. 通过判断环境选择不同的adapter,在浏览器中使用的是xhr.js
  4. xhr.js中展示了使用XHR对象请求的过程以及事件处理,还有promise的封装过程。

2.3 axios 返回的是 promise 对象

在研究axios如何发起http请求时我们已经得到了结果:

  • 使用了异步拦截器的请情况下,返回的是拦截器的promise
  • 未使用异步拦截器或者未使用拦截器,返回的是dispatchRequestpromise,底层是用promisexhr的封装。

在查看xhr.js的代码时,我对作者为什么要在onreadystatechangesetTimeout(onloadend);异步调用onloadend有些不解,看了作者的注释说是因为onreadystatechange会在ontimeout/onerror之前调用,所以如果这里同步调用的话,就会使用settle改变promise的状态了,但是作者希望不在settle中处理错误,而是通过xhr的事件去处理,因为我从来直接使用过xhr,所以我这里验证下:

var xhr = new XMLHttpRequest();

xhr.timeout = 1;
xhr.open('get', '/api', true);
xhr.ontimeout = function () {
    console.log('timeout & redystate = ', xhr.readyState);
}

xhr.onerror = function () {
    console.log('onerror & redystate = ', xhr.readyState);
}

xhr.onreadystatechange = function () {
    console.log('onreadystatechange & redystate = ', xhr.readyState);
}

xhr.send(null)

结果如下:


执行结果

总结下:xhr.js的过程中了解了一些使用XHR对象的坑,也借此机会去 MDN 上又重新学习了下XHR,获益匪浅~

三、总结及计划

总结

通过阅读代码,我了解到:

  • Axios整个项目的代码结构以及项目的大体思路;
  • Axios是如何管理拦截器的,方便下一步阅读拦截器代码;
  • Axios构造函数以及axios对象的属性以及方法;
  • Axios是如何使用Promise封装的;

计划

后续会按照官网给出的feature分步阅读源码:

  • Intercept request and response
  • Transform request and response data
  • Cancel requests
  • Automatic transforms for JSON data
  • Client side support for protecting against XSRF
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容

  • 前言 axios库提供了取消请求的功能,虽然个人平时很少用到。 正文 下面是官网的两个例子: 第一个例子中,在ax...
    ceido阅读 394评论 0 1
  • 项目中一直都有用到 Axios 作为网络请求工具,用它更要懂它,因此为了更好地发挥 Axios 在项目的价值,以及...
    DYBOY阅读 293评论 0 0
  • 基类 Axios 跟随入口 index.js 进入/lib/axios.js,第一个方法则是createInsta...
    丶梅边阅读 654评论 0 1
  • Axios是近几年非常火的HTTP请求库,官网上介绍Axios 是一个基于 promise 的 HTTP 库,可以...
    milletmi阅读 3,498评论 0 9
  • 实在来不及自己写了 把读过的文章先转过来 明天再进行编辑 axios项目目录结构 注:因为我们需要要看的代码都是...
    vivianXIa阅读 872评论 0 1