axios 源码简析

1,053 阅读6分钟

axios 简介

Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js。懂的都懂!

本文将从接下来的几个点,分析 axios:

  • 创建实例
  • 请求发送
  • 请求取消

创建实例

axios.js文件中会初始调用一次createInstance函数。在此函数中会new Axios创建一个名为context的实例和使用bind函数对Axios.prototype.request进行包裹并返回名为instance的函数。

然后,将Axios.prototypecontext的属性和方法extendinstance函数。最后,在instance上添加create方法。此方法是一个工厂方法,能够创建新的Axios实例。

源码 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);

  // Factory for creating new instances
  instance.create = function create(instanceConfig) {
    return createInstance(mergeConfig(defaultConfig, instanceConfig));
  };

  return instance;
}

// Create the default instance to be exported
var axios = createInstance(defaults);

通过上述步骤后,axios 变量可以通过axios({})发送请求,也可以通过axios.get/post等方式发起请求(get/post方法都是Axios.prototype上的方法)。

请求发送

我们调用的axiox.get/post等方法,其实都是去调用的Axios.prototype.request方法。在此方法中会合并配置、执行请求拦截器和响应拦截器等。

合并配置

源码 lib/core/Axios.js

Axios.prototype.request = function request(configOrUrl, config) {
  // 此处支持两种 api 的调用方式,axios('example/url'[, config]) 和 axios(config)
  if (typeof configOrUrl === 'string') {
    config = config || {};
    config.url = configOrUrl;
  } else {
    config = configOrUrl || {};
  }

  // 合并创建实例时传入的默认配置和目前传入的配置
  config = mergeConfig(this.defaults, config);
  ...
}

注意
通常我们在使用 axios 时会通过axios.defaults[name] = value来设置一些默认配置,作用于每一个请求。但是必须要在通过 axios.create 工厂方法创建实例之前设置。

// 正确用法
axios.defaults['timeout'] = 3000;
console.log('axios.defaults', JSON.parse(JSON.stringify(axios.defaults)));

const instance = axios.create({});
console.log('instance.defaults', JSON.parse(JSON.stringify(instance.defaults)));

image.png


// 错误用法
const instance = axios.create({});
console.log('instance.defaults', JSON.parse(JSON.stringify(instance.defaults)));

axios.defaults['timeout'] = 3000;
console.log('axios.defaults', JSON.parse(JSON.stringify(axios.defaults)));

image.png

请求拦截器

请求拦截器在我们日常开发中经常会用到。比如说我们在发送请求前记录发送时间(记录接口响应所花的时间),或者添加业务 token 之类的。请求拦截器目前分为两类:异步拦截器同步拦截器

源码 lib/core/Axios.js

// 收集请求拦截器

// 请求拦截器数组
var requestInterceptorChain = [];
// 请求拦截器同步还是异步执行
var synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
  synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;

  requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});

异步拦截器

源码 lib/core/Axios.js

var promise;

// 请求拦截器异步流程
if (!synchronousRequestInterceptors) {
  var chain = [dispatchRequest, undefined];

  // chain:[onfulfilled, onRejected, ..., dispatchRequest, undefined]
  Array.prototype.unshift.apply(chain, requestInterceptorChain);

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

  return promise;
}

通过 promise 的链式调用来实现异步请求拦截器。生成的 promise 链如下:

var promise = Promise.resolve(config);
promise
  .then("请求拦截器函数2", "请求拦截器异常处理函数2")
  .then("请求拦截器函数1", "请求拦截器异常处理函数1")
  ... // 可能有多个请求拦截器,也可能没有
  .then(dispatchRequest, undefined)
  .then("请求成功处理函数", "请求失败处理函数")

但是这样的方式也带了问题:

  1. 如果有请求拦截器的话,位于 promise 链中的第一个异常处理函数,永远都不会执行。(如上面的请求拦截器异常处理函数2)
  2. 如果主线程阻塞,ajax 请求会被延迟发送。(涉及 event Loop,可以去单独了解) 示例代码
axios.interceptors.request.use((config) => {
  return config;
}, (err) => {
  return Promise.reject(err);
});

// 将被延迟发送
axios.get('https://httpbin.org/get');

function manualRequest() {
  // 同步发送
  const xhr = new XMLHttpRequest();

  xhr.open('get', 'https://httpbin.org/get');
  xhr.send();
}

manualRequest();

// 模拟耗时操作
let x = 100000000;
while (x > 0) {
  x--;
}

image.png 从上图中可以看出,通过 axios 发出的请求会延迟。

同步拦截器

源码 lib/core/Axios.js

// 请求拦截器同步流程
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);
}

return promise;

同步拦截器就比较简单了。同时也没上面异步拦截器的问题。

示例代码

axios.interceptors.request.use((config) => {
  return config;
}, (err) => {
  return Promise.reject(err);
  // 使用同步拦截器
}, {synchronous: true});

axios.get('https://httpbin.org/get');

function manualRequest() {
  const xhr = new XMLHttpRequest();

  xhr.open('get', 'https://httpbin.org/get');
  xhr.send();
}

manualRequest();

// 模拟耗时操作
let x = 100000000;
while (x > 0) {
  x--;
}

image.png 从上图可以看出,使用同步拦截器时,请求不会延迟发送,两个请求基本上同时发出。

dispatchRequest

源码 lib/core/dispatchRequest.js

function dispatchRequest(config) {
 // 确定发送请求的模块
  var adapter = config.adapter || defaults.adapter;

  return adapter(config).then(function onAdapterResolution(response) {
  
    return response;
  }, function onAdapterRejection(reason) {

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

在这个函数中通过适配器模式确定发送请求的模块,然后发出请求。

adapter

源码 lib/defaults/index.js

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // 浏览器环境使用 XMLHttpRequest
    adapter = require('../adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // node 环境使用 HTTP/S
    adapter = require('../adapters/http');
  }
  return adapter;
}

通过适配器模式,识别不同的环境,兼容多种格式。对外提供统一的接口。

响应拦截器

源码 lib/core/Axios.js

var responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});

if (!synchronousRequestInterceptors) {
  var chain = [dispatchRequest, undefined];

  Array.prototype.unshift.apply(chain, requestInterceptorChain);
  chain = chain.concat(responseInterceptorChain);

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

  return promise;
}

响应拦截器也是通过 promise 的链式调用来实现的。生成的 promise 链如下:

var promise = Promise.resolve(config);
promise
  .then(dispatchRequest, undefined)
  .then("响应拦截器1", "响应拦截器异常处理函数1")
  .then("响应拦截器2", "响应拦截器异常处理函数2")
  .then("请求成功处理函数", "请求失败处理函数")

请求取消

axios 支持两种取消请求的方式。CancelToken 和 AbortController。在 v0.22.0 后开始,不推荐使用 CancelToken。

Cancel Token

源码 lib/cancel/CancelToken.js、lib/adapters/xhr.js

// CancelToken.js
function CancelToken(executor) {
  var resolvePromise;

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

  var token = this;

  this.promise.then(function(cancel) {
    if (!token._listeners) return;

    var i = token._listeners.length;

    while (i-- > 0) {
      token._listeners[i](cancel);
    }
    token._listeners = null;
  });

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

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

CancelToken.prototype.subscribe = function subscribe(listener) {
  if (this.reason) {
    listener(this.reason);
    return;
  }

  if (this._listeners) {
    this._listeners.push(listener);
  } else {
    this._listeners = [listener];
  }
};

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

// xhr.js
if (config.cancelToken) {
  onCanceled = function(cancel) {
    if (!request) {
      return;
    }
    reject(!cancel || (cancel && cancel.type) ? new CanceledError() : cancel);
    request.abort();
    request = null;
  };

  config.cancelToken && config.cancelToken.subscribe(onCanceled);
}

示例代码

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 {
    // 处理错误
  }
});

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

取消请求的流程比较复杂:

  1. 通过const source = CancelToken.source();生成一个 source 对象,source 对象中 token 属性是一个CancelToken实例,cancel 是取消 token 代表的 promise 实例的函数。
  2. 将 source 对象的 token 传递给请求的 config 中。
  3. xhr.js中,如果判断 config.cancelToken 为真,将 onCanceled 函数订阅到 source.token 对象的_listeners数组中。
  4. 当调用 source.cancel() 时,将 source.token 对象内的 promise 状态改为 fulfilled,然后执行 then 函数,遍历 source.token 对象的_listeners数组,然后执行每一个 onCanceled 函数。
  5. 执行 onCanceled 函数,将 XMLHttpRequest 实例对象 reject 和 abort 请求。

AbortController

源码 lib/adapters/xhr.js

if (config.cancelToken || config.signal) {
  onCanceled = function(cancel) {
    if (!request) {
      return;
    }
    reject(!cancel || (cancel && cancel.type) ? new CanceledError() : cancel);
    request.abort();
    request = null;
  };

  if (config.signal) {
    config.signal.aborted ? onCanceled() : config.signal.addEventListener('abort', onCanceled);
  }
}

示例代码

const controller = new AbortController();

axios.get('/foo/bar', {
  signal: controller.signal
}).then(function(response) {
  //...
});
// 取消请求
controller.abort()

AbortController的取消请求流程相对简单一点:

  1. 通过const controller = new AbortController();生成一个 controller 对象。
  2. 将 controller 对象的 singal 传递给请求的 config 中。
  3. xhr.js中,如果判断 config.singal 为真,将订阅 config.singal 的 abort 事件。
  4. 当执行 controller.abort() 时,触发订阅的 abort 事件。执行 onCanceled 函数,将 XMLHttpRequest 实例对象 reject 和 abort 请求。

目前只看了这么多,以后有时间再完善!