axios究竟有什么牛的呀?

3,592 阅读4分钟

axios究竟有什么牛的呀?

对于前端开发者来说,axios可谓是无人不知无人不晓的最热门的请求库之一了。在axios的官方文档中我们可以知晓它的一些过人之处,如:兼容浏览器、Node端,拦截器,取消请求等。下面我们就从源码的角度去探索一下axios究竟有什么牛的吧。

在阅读源码之前,我们一起来思考一些对axios的主要疑惑?

  1. axios执行的基本流程是什么?
  2. axios如何实现adapter?
  3. axios如何实现cancellation?
  4. axios如何实现processCapture?
  5. axios如何实现Intercepters?

本文从以上5个问题来探求axios的源码实现。

1.axios执行的基本流程

基础类Axios

  • 入口文件

首先我们先找到axios的入口:/lib/axios.js,可以看到我们平时用到的axios其实是这样的:

/**
 * Create an instance of Axios
 *
 * @param {Object} defaultConfig The default config for the instance
 *
 * @returns {Axios} A new instance of Axios
 */
function createInstance(defaultConfig) {
  const context = new Axios(defaultConfig);
  // 将最核心的request方法绑在实例上
  const instance = bind(Axios.prototype.request, context);

  // 将一些原型方法(get、request、post...)绑在实例上
  utils.extend(instance, Axios.prototype, context, {allOwnKeys: true});

  // 将一些属性(adapter、Intercepters...)绑在实例上
  utils.extend(instance, context, null, {allOwnKeys: true});

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

  return instance;
}

// Create the default instance to be exported
const axios = createInstance(defaults);
......
export default axios;

上述代码简而言之是将Axios原型的各种方法、属性绑定在了instance实例上。我们导出并使用的axios就是一个实例

  • Axios类

lib/core/Axios.js中,最基础的Axios类就被编写在这里:

/**
 * Create a new instance of Axios
 *
 * @param {Object} instanceConfig The default config for the instance
 *
 * @return {Axios} A new instance of Axios
 */
class Axios {
  constructor(instanceConfig) {
    // 默认配置
    this.defaults = instanceConfig;
    // 拦截器
    this.interceptors = {
      request: new InterceptorManager(),
      response: new InterceptorManager()
    };
  }

  /**
   * Dispatch a request
   *
   * @param {String|Object} configOrUrl The config specific for this request (merged with this.defaults)
   * @param {?Object} config
   *
   * @returns {Promise} The Promise to be fulfilled
   */
  request(configOrUrl, config) {
    /*eslint no-param-reassign:0*/
    
    // 【核心】处理各种配置
    // Allow for axios('example/url'[, config]) a la fetch API
    if (typeof configOrUrl === 'string') {
      config = config || {};
      config.url = configOrUrl;
    } else {
      config = configOrUrl || {};
    }

    config = mergeConfig(this.defaults, config);

    const {transitional, paramsSerializer, headers} = config;

  
    // Set config.method
    config.method = (config.method || this.defaults.method || 'get').toLowerCase();

    let contextHeaders;

    // Flatten headers
    contextHeaders = headers && utils.merge(
      headers.common,
      headers[config.method]
    );
    
    // 将默认的Headers和用户自定义Headers合并
    config.headers = AxiosHeaders.concat(contextHeaders, headers);

    // 过滤掉跳过的拦截器
    const requestInterceptorChain = [];
    let synchronousRequestInterceptors = true;
    this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
      if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
        return;
      }
      synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
      
      requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
    });

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

    let promise;
    let i = 0;
    let len;

    if (!synchronousRequestInterceptors) {
      const chain = [dispatchRequest.bind(this), undefined];
      chain.unshift.apply(chain, requestInterceptorChain);
      chain.push.apply(chain, responseInterceptorChain);
      len = chain.length;

      promise = Promise.resolve(config);

      while (i < len) {
        promise = promise.then(chain[i++], chain[i++]);
      }

      return promise;
    }

    len = requestInterceptorChain.length;

    let newConfig = config;

    i = 0;

    while (i < len) {
      const onFulfilled = requestInterceptorChain[i++];
      const onRejected = requestInterceptorChain[i++];
      try {
        newConfig = onFulfilled(newConfig);
      } catch (error) {
        onRejected.call(this, error);
        break;
      }
    }

    // 【核心】真正请求发送的地方~!
    try {
      promise = dispatchRequest.call(this, newConfig);
    } catch (error) {
      return Promise.reject(error);
    }

    i = 0;
    len = responseInterceptorChain.length;

    while (i < len) {
      promise = promise.then(responseInterceptorChain[i++], responseInterceptorChain[i++]);
    }

    return promise;
  }
  // 生成最终的请求URL
  getUri(config) {
    config = mergeConfig(this.defaults, config);
    const fullPath = buildFullPath(config.baseURL, config.url);
    return buildURL(fullPath, config.params, config.paramsSerializer);
  }
}

// 将一些get、post方法暴露出来
// 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,
      url,
      data: (config || {}).data
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  function generateHTTPMethod(isForm) {
    return function httpMethod(url, data, config) {
      return this.request(mergeConfig(config || {}, {
        method,
        headers: isForm ? {
          'Content-Type': 'multipart/form-data'
        } : {},
        url,
        data
      }));
    };
  }
  Axios.prototype[method] = generateHTTPMethod();
  Axios.prototype[method + 'Form'] = generateHTTPMethod(true);
});

我们可以关注到,整个Axios类只包括了两个属性和两个方法:

  • 属性:defaults默认配置、interceptors拦截器
  • 方法:request最核心的请求方法、getUri生成最终的请求URL

并且在最后将一些通用的getpost方法挂载在原型上。

了解完这些基础知识后,我们以axios.get('/user/info')这个最简单的GET请求来追溯一下axios的执行流程。

1. 执行get方法

  • 首先将参数传入最核心的request方法中

  • request方法中,依次对urlmethodtransitional, paramsSerializer, headers以及拦截器做配置处理,包括但不限于合并默认配置、过滤不必要的配置等操作。

这里的transitional是向后兼容配置,新版本将会移除。具体可见:github.com/axios/axios…

  • 执行拦截器(这里先不赘述)
  • 执行请求(dispatchRequest)

2. 发送请求

让我们来看一下dispatchRequest方法里面到底藏着什么玄机。(路径为/lib/core/dispatchRequest.js

/**
 * Dispatch a request to the server using the configured adapter.
 *
 * @param {object} config The config that is to be used for the request
 *
 * @returns {Promise} The Promise to be fulfilled
 */
export default function dispatchRequest(config) {
  throwIfCancellationRequested(config);

  config.headers = AxiosHeaders.from(config.headers);

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

  if (['post', 'put', 'patch'].indexOf(config.method) !== -1) {
    config.headers.setContentType('application/x-www-form-urlencoded', false);
  }

  // 【核心】适配器
  const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

  return adapter(config).then(function onAdapterResolution(response) {
   // 响应处理...

    return response;
  }, function onAdapterRejection(reason) {
    // 错误处理...

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

dispatchRequest的执行流程如下:

而适配器就是可以兼容浏览器和Nodejs环境进行发起网络请求的核心。如在浏览器中使用XMLHttpRequest来进行请求,最后返回一个Promise。

这就是整个Axios的最简单的执行流程!

2.axios如何实现adapter

每一个前端人都应该背过面试题:Axios在Node环境下使用的是http模块,而在浏览器环境下使用的是XMLHttpRequestApi,但是大家都知道这层适配器是实现原理嘛?

而最关键的适配器的实现代码如下lib/adapter/adapter.js

const knownAdapters = {
  http: httpAdapter,
  xhr: xhrAdapter
}

utils.forEach(knownAdapters, (fn, value) => {
  if(fn) {
    try {
      Object.defineProperty(fn, 'name', {value});
    } catch (e) {
      // eslint-disable-next-line no-empty
    }
    Object.defineProperty(fn, 'adapterName', {value});
  }
});

export default {
  getAdapter: (adapters) => {
    adapters = utils.isArray(adapters) ? adapters : [adapters];

    const {length} = adapters;
    let nameOrAdapter;
    let adapter;

    for (let i = 0; i < length; i++) {
      nameOrAdapter = adapters[i];
      if((adapter = utils.isString(nameOrAdapter) ? knownAdapters[nameOrAdapter.toLowerCase()] : nameOrAdapter)) {
        break;
      }
    }

    if (!adapter) {
      if (adapter === false) {
        throw new AxiosError(
          `Adapter ${nameOrAdapter} is not supported by the environment`,
          'ERR_NOT_SUPPORT'
        );
      }

      throw new Error(
        utils.hasOwnProp(knownAdapters, nameOrAdapter) ?
          `Adapter '${nameOrAdapter}' is not available in the build` :
          `Unknown adapter '${nameOrAdapter}'`
      );
    }

    if (!utils.isFunction(adapter)) {
      throw new TypeError('adapter is not a function');
    }

    return adapter;
  },
  adapters: knownAdapters
}

首先让我们回到前一节所提到的入口代码:

const adapter = adapters.getAdapter(config.adapter || defaults.adapter);

其中,这里的defaults.adapter值为['xhr', 'http']

然后通过判断knownAdapters中的值来去获取相应的请求函数。

那么注意,在axios判断处于那种环境的函数如下:

  • Node环境:

在Nodejs环境下有一个独一无二的全局对象:process,它存储着很多环境变量,我们可以打印一下process.toString()看看,结果如下:[Object process]

这也是axios依次判断环境的重要依据:

const isHttpAdapterSupported = typeof process !== 'undefined' && utils.kindOf(process) === 'process';
  • 浏览器环境

axios通过XMLHttpRequest的存在与否来判断是否处于浏览器环境:

const isXHRAdapterSupported = typeof XMLHttpRequest !== 'undefined';

我们知道了适配逻辑后,首先看看在浏览器环境下,axios对XMLHttpRequest Api的运用有多么出神入化吧~

1. 浏览器环境:XMLHttpRequest

首先,将传入的config对象中的headerdata取出做处理。其中,对data是否是formData类型(文件)做了一些判断:

if (utils.isFormData(requestData)) {
  // 判断在PC端还是移动端。
  if (platform.isStandardBrowserEnv || platform.isStandardBrowserWebWorkerEnv) {
    requestHeaders.setContentType(false); // Let the browser set it
  } else {
    requestHeaders.setContentType('multipart/form-data;', false); // mobile/desktop app frameworks
  }
}

我们来看看判断是否在PC端axios如何来做的:

/**
 * Determine if we're running in a standard browser environment
 *
 * This allows axios to run in a web worker, and react-native.
 * Both environments support XMLHttpRequest, but not fully standard globals.
 *
 * web workers:
 *  typeof window -> undefined
 *  typeof document -> undefined
 *
 * react-native:
 *  navigator.product -> 'ReactNative'
 * nativescript
 *  navigator.product -> 'NativeScript' or 'NS'
 *
 * @returns {boolean}
 */
const isStandardBrowserEnv = (() => {
  let product;
  if (typeof navigator !== 'undefined' && (
    (product = navigator.product) === 'ReactNative' ||
    product === 'NativeScript' ||
    product === 'NS')
  ) {
    return false;
  }

  return typeof window !== 'undefined' && typeof document !== 'undefined';
})();

/**
 * Determine if we're running in a standard browser webWorker environment
 *
 * Although the `isStandardBrowserEnv` method indicates that
 * `allows axios to run in a web worker`, the WebWorker will still be
 * filtered out due to its judgment standard
 * `typeof window !== 'undefined' && typeof document !== 'undefined'`.
 * This leads to a problem when axios post `FormData` in webWorker
 */
 const isStandardBrowserWebWorkerEnv = (() => {
  return (
    typeof WorkerGlobalScope !== 'undefined' &&
    self instanceof WorkerGlobalScope &&
    typeof self.importScripts === 'function'
  );
})();

axios主要是通过navigator.product来获取当前平台信息。当然,这个属性官方已经标注为废弃,现在可以使用useragent来获取更丰富的浏览器信息;同时,通过WorkerGlobalScope可以判断当前环境是否处于webworker中。

当浏览器接收到一个带有 FormData 对象的请求时,它会自动将请求头的 content-type 属性设置为 “multipart/form-data,因此我们并不需要手动添加。

接下来,axios还会根据config.auth来去配置Authorization。

然后构造一个XMLHttpRequest对象:

let request = new XMLHttpRequest();
// 这里request.open的第三个参数是async,表示是否异步。这里为true是可以避免阻塞
request.open(config.method.toUpperCase(), buildURL(...), true);

// Set the request timeout in MS
request.timeout = config.timeout; // 设置超时时间

初始化请求后,就开始了各种XMLHttpRequest参数、事件的配置,下面一起来看一下所用到的xhr的事件吧:

  • onloadend: axios用onloadend来代替onreadystatechange事件。用于处理响应数据。

回顾一下xhr.readyState的四种状态:

function onloadend() {
  if (!request) {
    return;
  }
  // Prepare the response
  const responseHeaders = AxiosHeaders.from(
    'getAllResponseHeaders' in request && request.getAllResponseHeaders()
  );
  
  const responseData = !responseType || responseType === 'text' || responseType === 'json' ?
    request.responseText : request.response;
  const response = {
    data: responseData,
    status: request.status,
    statusText: request.statusText,
    headers: responseHeaders,
    config,
    request
  };
  
  // Promise中的回调函数
  settle(function _resolve(value) {
    resolve(value);
    done();
  }, function _reject(err) {
    reject(err);
    done();
  }, 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
    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);
  };
}

这段代码我们也可以学习到一些冷知识:

  1. 响应数据如果是json或者文本则存在xhr.responseText
  2. 如果使用请求协议为file的话,即使成功,响应code也是0

之后这里对浏览器取消、网络请求错误以及请求超时的情况抛出了错误。

  • onabort: 当 request 被停止时触发,例如当程序调用 XMLHttpRequest.abort() 时。 也可以使用 onabort 属性。

  • onerror:当 request 遭遇错误时触发。

  • ontimeout:当 request 超时错误时触发


// 浏览器的取消请求,而非手动取消
// Handle browser request cancellation (as opposed to a manual cancellation)
request.onabort = function handleAbort() {
  if (!request) {
    return;
  }

  reject(new AxiosError('Request aborted', AxiosError.ECONNABORTED, config, request));

  // Clean up request
  request = null;
};

// Handle low level network errors
request.onerror = function handleError() {
  // Real errors are hidden from us by the browser
  // onerror should only fire if it's a network error
  reject(new AxiosError('Network Error', AxiosError.ERR_NETWORK, config, request));

  // Clean up request
  request = null;
};

// Handle timeout
request.ontimeout = function handleTimeout() {
  let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded';
  const transitional = config.transitional || transitionalDefaults;
  if (config.timeoutErrorMessage) {
    timeoutErrorMessage = config.timeoutErrorMessage;
  }
  reject(new AxiosError(
    timeoutErrorMessage,
    transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED,
    config,
    request));

  // Clean up request
  request = null;
};

往后,axios准备构造请求所需要的各种信息,包括请求头cookie等。

我们可以学习到:

  1. xhr.setRequestHeader(key, val):此方法用于设置HTTP请求头部,必须在 open() 方法和 send() 之间调用。
// Add headers to the request
if ('setRequestHeader' in request) {
  utils.forEach(requestHeaders.toJSON(), function setRequestHeader(val, key) {
    request.setRequestHeader(key, val);
  });
}
  1. xhr.withCredentials:该属性指示了是否该使用类似 cookie、Authorization 标头或者 TLS 客户端证书等凭据进行跨域访问(Acess-Control)请求。而且,当值为false的时候,来自不同域的xhr响应无法为其域设置Cookie。当值为true时,获取的第三方cookie 仍然遵循同源协议,因此请求的脚本无法通过 document.cookie 或者响应标头访问。

  2. xhr.responseType:指定响应中包含的数据类型。

// 一般不需要手动设置
if (responseType && responseType !== 'json') {
    request.responseType = config.responseType;
  }

倒数第二步,axios对文件上传、下载的进度监听以及请求的取消也作了支持:

  • xhr.onprogress:当请求接收到数据时被周期性触发(下载场景)。

  • xhr.upload:该属性是一个XMLHttpRequestUpload 对象,支持onloadstartonabortonprogress等事件监听,用于表示上传文件的进度。

  • xhr.abort():如果该请求已被发出,XMLHttpRequest.abort() 方法将终止该请求。

最后,axios对传入的URL协议进行了校验,并且通过request.send(requestData)发送请求。

整个流程示意图如下:

2. Node环境:http模块

Node下的http请求有点复杂且不常用,这里只做简易说明:

// 使用events模块来实现事件监听
const emitter = new EventEmitter();

// 事件注册
emitter.once('abort', reject);

// 协议解析
const parsed = new URL(fullPath, 'http://localhost');

const protocol = parsed.protocol || supportedProtocols[0];

// header设置
// ......

// 流式处理data

if (utils.isSpecCompliantForm(data)) {// 支持符合规范的FormData对象
  data = formDataToStream(data, (formHeaders) => {
    headers.set(formHeaders);
  });
  // support for https://www.npmjs.com/package/form-data api
} else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) {
 //...
} else if (utils.isBlob(data)) {
  data.size && headers.setContentType(data.type || 'application/octet-stream');
  headers.setContentLength(data.size || 0);
  data = stream.Readable.from(readBlob(data));
} else if (data && !utils.isStream(data)) {
  if (Buffer.isBuffer(data)) {
    // Nothing to do...
  } else if (utils.isArrayBuffer(data)) {
    data = Buffer.from(new Uint8Array(data));
  } else if (utils.isString(data)) {
    data = Buffer.from(data, 'utf-8');
  } else {
    // error
  }
  

// transport: http或者https模块
// Create the request
req = transport.request(options, handler)

req.on('error', ...);
// set tcp keep alive to prevent drop connection by peer
req.on('socket',...)

3.axios如何实现cancellation?

在了解axios如何终止请求之前,我们先来看一下Web环境下,使用fetch api进行终止请求的例子:

abortBtn.addEventListener("click", () => {
  if (controller) {
    controller.abort();
    console.log("中止下载");
  }
});

function fetchVideo() {
  controller = new AbortController();
  const signal = controller.signal;
  fetch(url, { signal })
    .then((response) => {
      console.log("下载完成", response);
    })
    .catch((err) => {
      console.error(`下载错误:${err.message}`);
    });
}

上述例子通过AbortController构造函数,实现了请求终止。AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。

那么在axios中,也是利用了AbortController接口,我们以下面的简单例子来打断点一步一步来探讨axios是如何中断请求的吧。

const controller = new AbortController();
axios.get('/foo/bar', {
  signal: controller.signal
})
// cancel the request
controller.abort()

还记得在dispatchRequest方法中最开始的throwIfCancellationRequested(config)函数嘛:

function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }

  if (config.signal && config.signal.aborted) {
    throw new CanceledError(null, config);
  }
}

这里对当前的请求状态做了判断,如果已经是被中断了,则直接抛出CanceledError。 但是目前我们的config.signal的状态是仍未被中断,因此继续执行:

{
  aborted: false, // 还没被中断
  onabort: null 
  reason: undefined
}

之后,在XMLHTTPRequest建立的过程中,还设置了abort的事件监听:

if (config.cancelToken || config.signal) {
  // Handle cancellation
  // eslint-disable-next-line func-names
  onCanceled = cancel => {
    if (!request) {
      return;
    }
    reject(!cancel || cancel.type ? new CanceledError(null, config, request) : cancel);
    request.abort();
    request = null;
  };

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

config.signal存在时,给config.signal.onabort设置回调函数onCanceled,用于监听AbortController.abort()触发的时候,执行xhr.abort()来终止HTTP请求,从而执行xhr.onabort的回调函数。

目前,我们还未中断请求,因此只会给signal绑定上事件监听器。

然后我们运行controller.abort(),此时config.signal.aborted为true,触发signal身上的onCanceled函数。

总而言之,AbortController相当于外层的事件总线,监听自身的abort()方法,当AbortController.abort()触发后,才会去执行xhr.abort(),最终执行XMLHTTPRequest的onabort钩子。

4. axios如何实现ProgressCaptrue

axios如何实现进度功能,其实在上述步骤中已经明了了。主要是通过xhr.onprogress以及xhr.upload.onprogress两个事件来实现。

axios中对于进度的处理函数如下:


function progressEventReducer(listener, isDownloadStream) {
  // 已经传入的字节
  let bytesNotified = 0;
  const _speedometer = speedometer(50, 250);

  return e => {
    const loaded = e.loaded;
    const total = e.lengthComputable ? e.total : undefined;
    const progressBytes = loaded - bytesNotified;
    const rate = _speedometer(progressBytes);
    const inRange = loaded <= total;

    bytesNotified = loaded;

    const data = {
      loaded,
      total,
      progress: total ? (loaded / total) : undefined,
      bytes: progressBytes,
      rate: rate ? rate : undefined,
      estimated: rate && total && inRange ? (total - loaded) / rate : undefined,
      event: e
    };

    data[isDownloadStream ? 'download' : 'upload'] = true;

    listener(data);
  };
}

5. axios如何实现Intercepters?

axios提供了一个非常灵活的中间件系统,可以使用拦截器来处理请求和响应。 拦截器是一个可选的中间件,可以在请求或响应的生命周期中调用,并且可以在请求或响应的任何阶段进行干预。

下面是一个官方的使用案例:

// Add a request interceptor
axios.interceptors.request.use(function (config) {
  // Do something before request is sent
  return config;
}, function (error) {
  // Do something with request error
  return Promise.reject(error);
});

// Add a response interceptor
axios.interceptors.response.use(function (response) {
  // Do something with response data
  return response;
}, function (error) {
  // Do something with response error
  return Promise.reject(error);
});

首先我们需要知道,axios.interceptors是一个什么东西?还记得在第一节中,Axios构造函数中的属性有哪些嘛。其中就包含着拦截器~

 this.interceptors = {
  request: new InterceptorManager(),
  response: new InterceptorManager()
};

我们可以在lib/core/InterceptorManager.js中找到InterceptorManager类:

class InterceptorManager {
  constructor() {
    this.handlers = [];
  }

  /**
   * 往栈中添加拦截器
   *
   * @param {Function} fulfilled The function to handle `then` for a `Promise`
   * @param {Function} rejected The function to handle `reject` for a `Promise`
   *
   * @return {Number} An ID used to remove interceptor later
   */
  use(fulfilled, rejected, options) {
    this.handlers.push({
      fulfilled,
      rejected,
      synchronous: options ? options.synchronous : false,
      runWhen: options ? options.runWhen : null
    });
    return this.handlers.length - 1;
  }

  /**
   * 移除对应id的拦截器
   *
   * @param {Number} id The ID that was returned by `use`
   *
   * @returns {Boolean} `true` if the interceptor was removed, `false` otherwise
   */
  eject(id) {
    if (this.handlers[id]) {
      this.handlers[id] = null;
    }
  }

  /**
   * Clear all interceptors from the stack
   *
   * @returns {void}
   */
  clear() {
    if (this.handlers) {
      this.handlers = [];
    }
  }

  /**
   * Iterate over all the registered interceptors
   *
   * This method is particularly useful for skipping over any
   * interceptors that may have become `null` calling `eject`.
   *
   * @param {Function} fn The function to call for each interceptor
   *
   * @returns {void}
   */
  forEach(fn) {
    utils.forEach(this.handlers, function forEachHandler(h) {
      if (h !== null) {
        fn(h);
      }
    });
  }
}

拦截器设置好后,当触发axios请求的时候,在Axios.request方法中:

// filter out skipped interceptors
const requestInterceptorChain = [];
let synchronousRequestInterceptors = true;
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
  // 拦截器还可以设置是否执行
  if (typeof interceptor.runWhen === 'function' && interceptor.runWhen(config) === false) {
    return;
  }
  // 是否允许拦截器同步执行
  synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
  // 【注意这里是unshift】注入到InterceptorChain中。此时的Chain为[fulfilled, rejected]
  requestInterceptorChain.unshift(interceptor.fulfilled, interceptor.rejected);
});

const responseInterceptorChain = [];
this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  // 【注意这里是push】
  responseInterceptorChain.push(interceptor.fulfilled, interceptor.rejected);
});


 if (!synchronousRequestInterceptors) {
  const chain = [dispatchRequest.bind(this), undefined];
  chain.unshift.apply(chain, requestInterceptorChain);
  chain.push.apply(chain, responseInterceptorChain);
  len = chain.length;

  promise = Promise.resolve(config);

  while (i < len) {
    promise = promise.then(chain[i++], chain[i++]);
  }
  return promise;
}

这里通过数组来依次放置拦截器。让我们来好好探讨一下这里的放置顺序问题。以上述官方代码为例,此时chain中的顺序为[requestInterceptorFulfilled, requestInterceptorRejected, dispatchRequest, undefined, responseInterceptorFulfilled, responseInterceptorChainRejected]

这里要注意的是,如果你添加了多个拦截器,比如请求拦截器1、2以及响应拦截器3、4,则最后执行顺序为2,1,request,3,4

之后,通过promise = promise.then(...)的形式,设计成了一条promise调用链,便于使用者最后还是可以去用then,catch方法做处理。

一句话总结,axios通过数组的形式,让用户以promise.fulfilled、promise.rejected的形式来添加拦截器

总结

axios是一个很老很经典的请求库,源码也不多,相对来说比较容易阅读。

阅读完这篇文章,相信读者们也对axios内部的执行流程、适配器层、请求中断、进度捕获以及拦截器的实现有了一定的了解,在继续学习fetchumi-request等新的请求api、第三方库的过程中,也一定可以有所帮助的。

我是「盐焗乳鸽还要香锅」,喜欢我的文章欢迎关注噢。