阅读axios源码

阅读axios源码,可解释下列问题:
1.为什么axios既可以像函数一样调用,也可以使用别名,如axios.request、axios.post这种方式来发起请求呢?
2.创建axios的过程中发生了什么
3.interceptors拦截器是如何实现的
4.CancelToken是如何实现的
5.为什么axios在浏览器环境和node环境里都能被调用

之前有看vue源码的计划,太多了,看着发困,遂弃。
找个代码量没那么多的axios循序渐进。

若川视野文章启发,出此文记之

关于调试:调试的方法文章里面写得很清楚了。因为axios支持node调用,所以调试的时候,启动一个server 和一个client ,通过node代码调用axios的get和post方法发送请求,打断点一步步看执行的情况。

一、创建实例

客户端使用var axios = require('../index');得到axios实例发生了什么

首先进入的是lib/axios.js里面进行axios实例的初始化,关键代码如下。

// lib/axios.js
var axios = createInstance(defaults);
axios.Axios = Axios;
axios.create = function create(instanceConfig) {
    return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
axios.all = function all(promises) {
    return Promise.all(promises);
};
axios.spread = require('./helpers/spread');

可以带着第一个问题来看这个过程:为什么axios既可以像函数一样调用,也可以使用别名,如axios.request、axios.post这种方式来发起请求呢?

逐一分析:

1.defaults
创建实例传入的defaults,里面是对header、adapter、xsrfCookieName、xsrfHeaderName属性的赋值。特别地,初始化了headers的一些方法,以便使用时可以通过调用axios.defaults.headers的方式去查询、修改到默认的headers赋值

/* lib/defaults */
utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});

对照axios文档,使用场景是这样的

// 配置的默认值/defaults
// 你可以指定将被用在各个请求的配置默认值
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';

可以在使用时先设定好默认使用的参数,如果不主动设置将走默认配置
2.创建实例 createInstance(defaults)

/* lib/axios.js */
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;
}

这个函数解释了最初提出的问题,关于axios的调用方式灵活性。

  • 首先使用new Axios的方式去得到context这个对象。对象有两个属性:上文说的defaults、和拦截器interceptors。context将作为上下文,在之后不断被使用。
/* lib/core/Axios.js */
function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
  • 得到instance方法。var instance = bind(Axios.prototype.request, context);
    bind 这个方法很有意思,通过闭包的形式,实现传进参数后,执行以context为上下文的request方法。
// bind.js
module.exports = 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);
    };
};
  • 接下来是utils.extend(instance, Axios.prototype, context);
    目的是 遍历Axios.prototype的属性,逐一给instance赋值,如果该属性为方法,则以context对象为上下文。
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;
}

这里Axios.prototype的属性有:request、getUri还有别名'delete', 'get', 'head', 'options','post', 'put', 'patch'
别名们通过给request方法入参传入method属性来实现调用,本质上还是调用了request。所以axios({method:'get'})axios.get是一样的。

/* 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(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  /*eslint func-names:0*/
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});
  • 最后是把context里的属性和方法赋值给instance utils.extend(instance, context);

剩下的几步是一些赋值操作,比如允许继承的Axios,让用户自定义传入参数的create方法,用于取消请求操作的Cancel等,还有一个等同于Promise.all 的 axios.all方法

至此实例axios创建完成。

二、发送请求

上文提到,不论是将axios当做方法还是使用别名发起请求,最终调用的还是request方法,代码如下

/* lib/core/Axios.js */
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';
  }

  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

这个方法里的this,就是上文提到的context,内容如下


下面按顺序来分析,当调用了request时,做了些什么

  • method
    在这里首先判断了method是什么。如果传入了则使用传入的,也支持用户在axios.defaults.method设置默认的request方法,如果都没有读到,则默认的方法为get。
  • interceptors 拦截器
    interceptors是axios里比较常用的功能,用途为

在请求或响应被 then 或 catch 处理前拦截它们

在发送请求之前,我设置了

axios.interceptors.request.use(
    function requestSuccess(config) {
        // 在发送请求之前做些什么
        console.log('interceptors request');
        return config;
    },
    function requestEnd(error) {
        // 对请求错误做些什么
        return Promise.reject(error);
    }
);

axios.interceptors.response.use(
    function responseSuccess(config) {
        // 在response回来之前做些什么
        console.log('interceptors response');
        return config;
    },
    function responseEnd(error) {
        // 对请求错误做些什么
        return Promise.reject(error);
    }
);

客户端通过发送请求之前,调用use方法对InterceptorManager维护的handlers数组进行添加操作。
而在request方法里,对于拦截器的处理方法是——维护一个名为chain的成功失败回调数组。request 的interceptors方法在chain的头部插入,response的interceptors方法在chain的尾部插入
笔者设置了interceptors.requestinterceptors.response,可以看到此时的chain经处理后如下图

然后循环将这些用promise then 串起来,

while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
}

所以chain是成功失败回调一前一后排列的顺序

  • dispatchRequest
    在请求拦截器设置的回调执行完成之后,将会执行dispatchRequest,代码如下
/* lib/core/dispatchRequest */
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  // Ensure headers exist
  config.headers = config.headers || {};

  // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

  // Flatten headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers || {}
  );

  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

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

  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );

    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);

      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }

    return Promise.reject(reason);
  });
};

这个方法先是对入参config进行处理。比如在transformRequest里根据data的类型设置headers,删掉之前对headers添加的方法
最后执行adapter方法去发送请求。

  • adapter
    getDefaultAdapter 这个方法判断当前环境是node还是浏览器,选择对应的发送请求的方法。解释了为什么axios支持两个环境。
/* lib/defaults.js*/
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;
}
  • CancelToken 实现原理
    简单说来是在请求的过程中,通过执行XMLHttpRequest.abort()去中断当前请求。
    看之前有一个疑问,既然在请求时已经执行完xhr.js里的代码了,而这个cancelToken是在xhr.js里的。那要如何实现在请求发送之后,客户端可以调用CancelToken去中断这个请求的执行呢?
// 客户端代码
var CancelToken = axios.CancelToken;
var source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // 处理错误
  }
});

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');
// lib/cancel/CancelToken.js
'use strict';

var Cancel = require('./Cancel');

/**
 * A `CancelToken` is an object that can be used to request cancellation of an operation.
 *
 * @class
 * @param {Function} executor The executor function.
 */
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

/**
 * Throws a `Cancel` if cancellation has been requested.
 */
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

/**
 * Returns an object that contains a new `CancelToken` and a function that, when called,
 * cancels the `CancelToken`.
 */
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

module.exports = CancelToken;

// lib/adapters/xhr.js
if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }

从客户端发起请求说起。
CancelToken.source()执行之后,将得到一个对象{token:token,cancel:cancel}
token是CancelToken实例 ,这个实例拥有一个名为promise的属性;cancel是一个方法,用闭包的方式保存了创建实例时CancelToken的上下文。
让我们再仔细看CancelToken里的实现

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });
  executor(function cancel(message) {
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });

这个位于CancelToekn里的cancel就是实例里的cancel。如果执行它,将会执行resolvePromise,即触发this.promise的成功回调
又因为在发送请求时,xhr里做了这样的处理

// lib/adapters/xhr.js
  if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }

        request.abort();
        reject(cancel);
        // Clean up request
        request = null;
      });
    }

所以当config.cancelToken.promise的成功回调被触发,将会走到then的成功回调onCanceled里,request.abort()执行之后就可以达到中断当前请求的目的。

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

推荐阅读更多精彩内容

  • axios 是一个基于 Promise 的http请求库,可以用在浏览器和node.js中 备注: 每一小节都会从...
    Polaris_ecf9阅读 649评论 0 1
  • Axios是近几年非常火的HTTP请求库,官网上介绍Axios 是一个基于 promise 的 HTTP 库,可以...
    milletmi阅读 3,498评论 0 9
  • Axios是一个基于Promise的HTTP请求库,可以用在浏览器和Node.js中。平时在Vue项目中,经常使用...
    多啦斯基周阅读 874评论 0 0
  • 概述 在前端开发过程中,我们经常会遇到需要发送异步请求的情况。而使用一个功能齐全,接口完善的HTTP请求库,能够在...
    grain先森阅读 1,570评论 0 4
  • 今天看了意林2018第十七期的一篇文章,题目为《不要小看30天》。文章大意是30天的持续坚持做一件事,会有意想不到...
    中年人到阅读 167评论 0 1