今天看啥  ›  专栏  ›  Mengxixi

axios源码分析——学习笔记

Mengxixi  · 掘金  ·  · 2021-05-29 22:14
阅读 67

axios源码分析——学习笔记

写在前面

本文章是axios学习源码的笔记文章,原视频是尚硅谷的axios教学视频,链接如下:www.bilibili.com/video/BV1NJ…

axios与Axios的关系

  1. Axios是构造函数,axios是一个函数
  2. 语法上来说axios不是由Axios构建的实例,但是功能上来说axios具有Axios实例的功能(属性和方法)
  3. axios是Axios.prototype.request函数bind()返回的函数

图解:

image.png

axios与 axios.create返回的instance区别

  1. axios多了create,CancelToken,all等方法
  2. axios和instance的配置(axios.defaults+createConfig)可能不一样

axios.js代码段:

function createInstance(defaultConfig) {
  //创建Axios实例对象context
  var context = new Axios(defaultConfig);
  //将Axios.prototype.request函数的this指向绑定context,
  //并将该新函数返回赋值给instance
  debugger
  var instance = bind(Axios.prototype.request, context);

  //将原型上的方法添加到instance上
  utils.extend(instance, Axios.prototype, context);

  // 将context(Axios原型实例)上的属性(defaults和interceptors)添加到instance上
  utils.extend(instance, context);

  return instance;
}

// 创建axios用于模块输出(调用了createInstance)
var axios = createInstance(defaults);

// Expose Axios class to allow class inheritance
//将Axios类暴露,从而实现类的继承
axios.Axios = Axios;

// 定义create方法
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// Expose Cancel & CancelToken
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// Expose all/spread
axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');

// Expose isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');
复制代码

axios运行的整体流程

image.png 从图中可以看出axios运行时最重要的三个函数:request(config) => dispatchRequest(config) => xhrAdapter(config)

request

代码段:

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;
};
复制代码

request函数的代码最主要的部分如下,这是request函数逻辑的重要实现部分:

  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;
复制代码

这段代码首先定义了一个chain数组,通过数组的unshift方法和push方法,分别将请求拦截器的回调函数推入chain的左边,将响应拦截器的回调函数推入chain的右边,而chain的中间是dispatchRequest函数。之后通过从左往右弹出chain数组元素进行promise链式调用完成了从请求拦截器到响应拦截器之间的工作。 举一个例子,我们准备了两个请求拦截器requestInt1和requestInt2,两个响应拦截器responseInt1和responseInt2,用图来解释这四个拦截器是如何放入chain数组中以及最后如何被链式调用的。

image.png 由图可知,由于promise链式调用是两两一组(拦截器的成功回调+失败回调),所以chain数组初始定义了一个undefined和dispatchRequest组合防止链式调用时乱序。

dispatchRequest

dispatchRequest模块的流程

转换请求数据 => 调用xhrAdapter()发请求 => 请求返回后转换响应数据并返回promise对象

转换请求数据

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

复制代码

该部分主要用于转换的函数为defaults模块的transformRequest函数,代码如下:

transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }]
复制代码

请求返回后转换响应数据

  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;
  }
复制代码

这部分的代码在adapter调用之后的promise回调中,用于转换的函数是defaults模块的transformResponse函数。

  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }]
复制代码

一开始作者不理解这一块的逻辑,后来试了一下,发现JSON数据的类型就是string格式,如果data是一个非JSON格式的字符串,则这段会报错,因此该函数选择了try cache的结构来防止程序报错。

xhrAdapter

xhrAdapter模块的流程

创建XHR对象(最底层,调用AJAX),根据config进行相应设置,发送特定请求,接受相应数据,返回promise。xhrAdapter模块的主要代码如下(从xhr.open到onreadystatechange部分):

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;

    if (utils.isFormData(requestData)) {
      delete requestHeaders['Content-Type']; // Let the browser set it
    }

    var request = new XMLHttpRequest();

    // HTTP basic authentication
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }

    var fullPath = buildFullPath(config.baseURL, config.url);
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

    // Set the request timeout in MS
    request.timeout = config.timeout;

    // Listen for ready state
    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;
      }

      // Prepare the response
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      var responseData = !config.responseType || config.responseType === 'text' ? 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;
    };
复制代码

可以看到该部分返回一个Promise对象,而该对象状态的判定交给了settle函数,该部分函数代码如下:

module.exports = function settle(resolve, reject, response) {
  var validateStatus = response.config.validateStatus;
  if (!response.status || !validateStatus || validateStatus(response.status)) {
    resolve(response);
  } else {
    reject(createError(
      'Request failed with status code ' + response.status,
      response.config,
      null,
      response.request,
      response
    ));
  }
};
复制代码

该部分的validateStatus是axios的config设定,定义了返回状态码的合法范围,默认范围为[200,299]。

取消请求

axios取消请求的实现牵涉到config中的canceltoken的配置,先来看看canceltoken的用法(cancel函数可以传入参数--取消原因):

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

复制代码

CancelTOken部分的源码如下:

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }
  //为取消请求准备了一个Promise对象promise,并把该对象的resolve函数存到外面
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });
  //保存当前的token对象
  var token = this;
  executor(function cancel(message) {
    //如果token中已有reason属性,说明取消已经被请求过
    if (token.reason) {
      // Cancellation has already been requested
      return;
    }
    //token.reason指定为一个Cancel对象
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}
复制代码

利用executor函数的参数,这里很巧妙地将CancelToken内部定义好的cancel函数传递给外部调用者,在需要取消请求时调用cancel函数,使得resolvePromise的状态变为fulfilled,值为reason。 而CancelToken的实例对象的canceltoken的promisethen方法的调用写在了xhr.js里面:

    if (config.cancelToken) {
      // Handle cancellation
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }
        //取消请求
        request.abort();
        //让请求的promise失败
        reject(cancel);
        // Clean up request
        request = null;
      });
    }
复制代码

需要注意的是,该部分的cancel不是之前的cancel函数,而是一个Cancel对象,是CancelToken.js模块的token.reason。 调用cancel()的具体细节如下:

image.png




原文地址:访问原文地址
快照地址: 访问文章快照