Axios源码初探

接着上篇 如何让axios在vue中丝滑起来。对axios的内部原理甚是好奇,近两天看了他的源码,分析了一些,确实值得80K的star。

另外,本文仅是浅浅的分析实现。至于有些地方为什么那样实现,代码设计的精妙,以及涉及到的一些设计模式,限于能力,遇到只能说句
卧槽!还可以这样写,绝啊!
在这里插入图片描述

⚾准备

随便找个测试项目 ,本次分析版本的是 @v0.21.0

cnpm install axios@0.21.0 

然后从node_modules里面找到 axios 一步步解开神秘的面纱。

既然看源码了,那么对axios的基本API⇲应该是很熟悉了。不然会增加理解成本!
本文配和源码享用,可能更香哦!

⚾源码结构

pakage.json 文件知道,index.js 是入口文件。
另外lib 是源代码文件, dist 是打包文件。
下面是文件目录(部分文件已被删除)

📦axios
 ┣ 📂dist
 ┣ 📂lib
 ┃ ┣ 📂adapters //请求方法
 ┃ ┃ ┣ 📜http.js // node环境请求
 ┃ ┃ ┣ 📜README.md
 ┃ ┃ ┗ 📜xhr.js // 浏览器环境请求 xmlRequest
 ┃ ┣ 📂cancel //取消请求相关
 ┃ ┃ ┣ 📜Cancel.js
 ┃ ┃ ┣ 📜CancelToken.js
 ┃ ┃ ┗ 📜isCancel.js
 ┃ ┣ 📂core
 ┃ ┃ ┣ 📜Axios.js // class Axios
 ┃ ┃ ┣ 📜buildFullPath.js 
 ┃ ┃ ┣ 📜createError.js // 统一的错误创建
 ┃ ┃ ┣ 📜dispatchRequest.js // 执行请求
 ┃ ┃ ┣ 📜enhanceError.js // error.toJSon
 ┃ ┃ ┣ 📜InterceptorManager.js //拦截器定义
 ┃ ┃ ┣ 📜mergeConfig.js // 配置合并函数
 ┃ ┃ ┣ 📜README.md
 ┃ ┃ ┣ 📜settle.js //
 ┃ ┃ ┗ 📜transformData.js // 数据转换
 ┃ ┣ 📂helpers //辅助工具函数
 ┃ ┃ ┣ 📜bind.js
 ┃ ┃ ┣ 📜buildURL.js
 ┃ ┃ ┣ 📜combineURLs.js
 ┃ ┃ ┣ 📜cookies.js
 ┃ ┃ ┣ 📜deprecatedMethod.js
 ┃ ┃ ┣ 📜isAbsoluteURL.js
 ┃ ┃ ┣ 📜isAxiosError.js
 ┃ ┃ ┣ 📜isURLSameOrigin.js
 ┃ ┃ ┣ 📜normalizeHeaderName.js //规范headers属性名
 ┃ ┃ ┣ 📜parseHeaders.js
 ┃ ┃ ┣ 📜README.md
 ┃ ┃ ┗ 📜spread.js
 ┃ ┣ 📜axios.js // 对外暴露接口
 ┃ ┣ 📜defaults.js
 ┃ ┗ 📜utils.js
 ┣ 📂node_modules
 ┣ 📜index.js //入口文件
 ┣ 📜package.json

可能有的朋友就要问了,靓仔,你这目录打印的怪好看呢,我怎么才能像你一样打印出如此美观的目录(windows下)。
为了满足这部分朋友,介绍两个 vsCode 插件🐶

  • file-tree-generator (推荐)
  • ascii Tree Genertaor(简约)

⚾如何实现请求的一条龙服务

初始化

首先,当

important axios from 'axios';

上面引用 index.js, index.js 引用 axios.js
🧾axios.js
前面时一个创建实例的方法,具体每一步做什么代码中已注释

function createInstance(defaultConfig) {
  // 创建一个axios的实例
  var context = new Axios(defaultConfig);
  // 将axios 的请求方法绑定为上面实例作为上下文,和bind一样
  var instance = bind(Axios.prototype.request, context);

  // 将原axios.prototype上方法复制到axios,并绑定上下文为context
  utils.extend(instance, Axios.prototype, context);

  // 将context的方法复制给instance
  utils.extend(instance, context);

  return instance;
}

上面方法什么时候用呢,在返回 axios, 或者调用 axios.create 的时候被调用!

// 创建一个默认导出的实例
var axios = createInstance(defaults);

// 暴露axios类以允许类的继承
axios.Axios = Axios;

// 用于创建新实例的工厂
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

// 暴露Cancel与token, 取消请求后面介绍
axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');

// axios 的all方法就是调用Promise.all
axios.all = function all(promises) {
  return Promise.all(promises);
};

axios.spread = require('./helpers/spread');

// Expose isAxiosError
axios.isAxiosError = require('./helpers/isAxiosError');

axios.js 做了什么事?

  • 定义了创建实例的方法
  • 补齐axios的属性/方法,并暴露出去
    上面 createInstance 里面,主要是 Axios 有很大的作用, 让我们来康康。

🧾Core/Axios.js

function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  // 定义了拦截器属性
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}

哦,原来 Axios 是个函数!需要传入一些配置作为它的 defaults 属性,这里的配置就是下面使用中传入的 config

axios(config)
axios.get(url, config);
axios.post(url, data, config);
var instance = axios.create(config);

接着来看,

Axios.prototype.request = function request(config) {
 // 这里的config是请求拦截器里面来的,下面做一些格式的处理
  if (typeof config === 'string') { // 是字符串就当做url处理
    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';
  }

  // 连接拦截器中间件,这里的dispatchRequest就是分发请求的方法,参考后面[分发请求]
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);// 这里传入初始的配置,可以供请求拦截器修改配置。
  
  //调用拦截器自定义方法forEach,参考后面[拦截器]
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    // interceptor 就是我们传入拦截器的两个函数,  然后插入chain数组开头
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    // 向chain数组末尾添加元素
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });
 
 //while循环一步步利用then链向下执行
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
    /*
      相当于 
      promise.then(request_fullfilled, request_rejected)
        .then(dispatchRequestFunc, undefined)
        .then(response_fullfilled, response_rejected)
    */
  }
  return promise;
};

哦,原来 axios.prototype 还有个 request 方法,后面 chain 链设计的很棒,我必须说一句
卧槽!还可以这样写,绝啊!

  • 满足了可添加多个拦截器
  • 满足了每一步都异步执行
  • 优雅优雅优雅!!

注意 | 每次从数组里面取的都是前两个(shift会改变数组)执行,这回造成当有多个拦截器的时候,请求拦截器先加入后执行,相应拦截器先加入先执行

补充一下拦截器的使用,为了方便理解,匿名函数我给了名字。

// 添加请求拦截器
axios.interceptors.request.use(
	function request_fullfilled(config) {return config},
	function request_rejected(error) { return Promise.reject(error)}
);
// 添加响应拦截器
axios.interceptors.response.use(
	function response_fullfilled(response) { return response},
	function response_rejected(error) { return Promise.reject(error)}
);

大家好好想想,文字描述不如看代码直接。
我当时看到这个设计,嘴里不断发出 啧~ 啧~ 啧~ 的赞叹!😱
在这里插入图片描述

继续看下面🐶

//原型上定义一个将请求参数格式化的方法。类似{a:1, b:2} 变成 a=1&b=2 的功能
Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
  return buildURL(config.url, config.params, config.paramsSerializer).replace(/^\?/, '');
};

// 将所有方法添加到Axios原型上,且相应的调用并返回注入配置的请求方法的返回值。
// 并将data添加到config.data上面,
// 为支持的请求方法提供别名
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
    }));
  };
});

注意最后 将 deletegetpost、… 方法添加到了原型上。这个文件相对比较重要,我认为有承前启后,贯穿上下的作用,所以做了详细介绍。那 Axios.js 做了什么事呢?

  • .deaults 属性保存了配置项
  • 配置了请求/响应拦截器
  • Axios.prototype 上定义了一些它方法(request, getUri, get, post…)

上面 forEach 方法是干的什么的呢,就像 Array.prototype.forEach 一样
Core/untils

/*
为obj的每一项执行一次fn。

如果'obj'是数组,则调用回调传递每个项的值、索引和完整数组。
如果“obj”是对象,则调用回调传递每个属性的值、键和完整对象。
*/
function forEach(obj, fn) {
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
    return;
  }
  if (typeof obj !== 'object') {
    obj = [obj];
  }
  //数组和对象分开处理
  if (isArray(obj)) {
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

每次调用 axios.get = > Axios.prototype.get => Axios.protype.request

注意Axios实例上的方法和原型上的方法都在axios.js createInstance 复制给了axios

所以 axios[method] 的语法糖也就好解释了,每次 Method 都是触发 request 方法来请求的。
request 先放放,下面来看看拦截器里面是什么东西

拦截器

在🧾Axios.js,有这样的代码

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

下面进入 InterceptorManager
🧾Core/InterceptorManager.js

// 定义一个拦截器方法,并且返回所在handers的位置下标作为清除的id
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};

// 移除拦截器的方法,将id置空
InterceptorManager.prototype.eject = function eject(id) {
  if (this.handlers[id]) {
    this.handlers[id] = null;
  }
};
// 定义一个forEach方法,对所有的拦截器就行迭代
InterceptorManager.prototype.forEach = function forEach(fn) {
  utils.forEach(this.handlers, function forEachHandler(h) {
    if (h !== null) { //确保拦截器未被移除
      fn(h); // h = {fulfilled: fulfilled, rejected: rejected}
    }
  });
};

代码不多,结合注释理解即可。
注意 | 这里的自定义 forEach方法axios.js 拦截器里使用过
接着我们来看request如何请求

分发请求

request 为啥能发出请求,主要是 dispatchRqeust。听说它很勇,走,一起来康康🤭。
🧾Core/dispactchRequest.js

// 如果配置了取消,则抛出取消信息,取消请求参考[带着问题加深理解]
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}
// 使用配置的适配器向服务器发送请求
module.exports = function dispatchRequest(config) {
  throwIfCancellationRequested(config);
  config.headers = config.headers || {};
  /*
    function transformData(data, headers, fns) {
      utils.forEach(fns, function transform(fn) {
        data = fn(data, headers);
      });
      return data;
    };
  */
  // 将config.headers的一些字段规范化,不规范的设置为无效取默认值。
  // 将config.data格式化,上面是transformData函数
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest // 如果没写,则来自 defaults 的默认配置,参见[默认配置]
  );

  // 打平配置头部, .common 参见[默认配置]
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );
  
  // 清除headers上的方法, 因为这些并不是headers的合法属性, 如果需后台允许headers自定义属性
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );
  //定义是适配器,可自定义
  var adapter = config.adapter || defaults.adapter;
  // adapter 返回的是个promise
  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);

    // 转换响应数据
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;
  }, function onAdapterRejection(reason) {
  	// 如果没取消
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
	   // 转换响应数据
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }
    return Promise.reject(reason);
  });
};

上面文件做啥了?

  • config配置的规范转换,与默认配置合并
  • 利用适配器 Adapter 发出请求,并将响应数据做转换。

注意|dispatchRequest 的结果会返回到响应拦截器(如果有)或者axios(config).then的回调
上面文件有两个关键的东西,一是 默认配置,二是 适配器,我们先看 默认配置

默认配置

默认配置主要在defaults.js里
🧾Core/defaults.js

var DEFAULT_CONTENT_TYPE = {
  'Content-Type': 'application/x-www-form-urlencoded'
};
// 设置默认的content-Type
function setContentTypeIfUnset(headers, value) {
  if (!utils.isUndefined(headers) && utils.isUndefined(headers['Content-Type'])) {
    headers['Content-Type'] = value;
  }
}
// 获取默认的适配器,不同环境调用不用
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') { // 对于浏览器,使用XHR适配器
    adapter = require('./adapters/xhr');
  } else if (
  	typeof process !== 'undefined' && 
  	Object.prototype.toString.call(process) === '[object process]') { //对于node使用HTTP适配器
    adapter = require('./adapters/http');
  }
  return adapter;
}

var defaults = {
  adapter: getDefaultAdapter(),
  // 默认转换请求数据函数,这里就是一些data,headers的转换
  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 重置headers的contentType
    //[了解更多⇲](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Type)
    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;
  }],
  //默认转换响应数据函数
  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],
  //超时时间,如果请求超过此时间(ms),会被取消,如果为0,则会一直请求。
  timeout: 0,
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  maxContentLength: -1,
  maxBodyLength: -1,

  // 关于状态码的解释: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
  // 默认是把status为 2xx 请求数据传入成功回调
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};
// 将get、delete...方法添加到 heades上面
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);
});

这文件干了什么?

  • 定义了一些初始值和配置数据转换的默认方法
  • 定义了根据环境获取适配器的方法

接着我们康康🤓适配器

适配器

适配器就是最接近发出请求的地方,这里不分析node环境的http方式,稍微分析一些xhr的方式.
先来个基本的xhr的请求。来自MDN⇲

var xhr= new XMLHttpRequest(),
    method = "GET",
    url = "https://developer.mozilla.org/";

xhr.open(method, url, true);
xhr.onreadystatechange = function () {
  if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
    console.log(xhr.responseText)
  }
}
xhr.send();

为节省篇幅,不多做介绍,这个知识点很大,建议 MDN⇲ 上去了解
🧾adapters/xhr.js

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
    }

    //关于XMLHttpRequest https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
    var request = new XMLHttpRequest();

    // http身份认证,有些请求你可以定制某些身份才可以访问
    if (config.auth) {
      var username = config.auth.username || '';
      var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
      requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
    }
    // 将基础baseUrl与请求url进行组合
    var fullPath = buildFullPath(config.baseURL, config.url);
    // 初始化一个请求
    request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);
    request.timeout = config.timeout;

    // XMLHttpRequest 的readyState 属性发生改变时触发 readystatechange (en-US) 事件的时候被调用
    request.onreadystatechange = function handleLoad() {
      // 关于readyState的状态,https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/readyState
      if (!request || request.readyState !== 4) {
        return;
      }
      if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
        return;
      }

      // 将头部字段解析为对象 getAllResponseHeaders
      //参考(https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/getAllResponseHeaders)
      var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
      var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
      //responseText 是一个纯文本的值, response 是由responseType决定,可能有多个值;
      // [参考]x(https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/response)
      var response = {
        data: responseData,
        status: request.status,
        statusText: request.statusText,
        headers: responseHeaders,
        config: config,
        request: request
      };

      /*
        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
          ));
        }
      };
      */
      // 由status来判断那些请求当做成功,那些当做失败
      settle(resolve, reject, response);
      request = null;
    };
    // 当一个请求终止时 abort 事件被触发
    request.onabort = function handleAbort() {
      if (!request) {
        return;
      }
      reject(createError('Request aborted', config, 'ECONNABORTED', request));
      request = null;
    };
    // 当请求遇到错误时,将触发error 事件。
    request.onerror = function handleError() {
      // Real errors are hidden from us by the browser
      // onerror should only fire if it's a network error
      reject(createError('Network Error', config, null, request));
      request = null;
    };

    // 当进度由于预定时间到期而终止时,会触发timeout 事件。
    request.ontimeout = function handleTimeout() {
      var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
      if (config.timeoutErrorMessage) {
        timeoutErrorMessage = config.timeoutErrorMessage;
      }
      reject(createError(timeoutErrorMessage, config, 'ECONNABORTED',
        request));
      request = null;
    };

    //添加xsrf标头只有在标准浏览器环境中运行时才能执行此操作。
    if (utils.isStandardBrowserEnv()) {
      var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ?
        cookies.read(config.xsrfCookieName) :
        undefined;
      if (xsrfValue) {
        requestHeaders[config.xsrfHeaderName] = xsrfValue;
      }
    }

    // 向请求中添加headers
    if ('setRequestHeader' in request) {
      utils.forEach(requestHeaders, function setRequestHeader(val, key) {
        if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
        //如果数据未定义,则删除内容类型
          delete requestHeaders[key];
        } else {
        //否则,将头添加到请求中
          request.setRequestHeader(key, val);
        }
      });
    }

    // 配置请求是否支持携带cookie
    if (!utils.isUndefined(config.withCredentials)) {
      request.withCredentials = !!config.withCredentials;
    }

    // 将responseType添加到请求中
    if (config.responseType) {
      //..
    }

    // 控制请求进度
    if (typeof config.onDownloadProgress === 'function') {
      request.addEventListener('progress', config.onDownloadProgress);
    }
    // 上传事件的兼容处理,增加健壮性
    if (typeof config.onUploadProgress === 'function' && request.upload) {
      request.upload.addEventListener('progress', config.onUploadProgress);
    }
	// 这里时取消请求的处理逻辑,参考[带着问题加深理解]
    if (config.cancelToken) {
      config.cancelToken.promise.then(function onCanceled(cancel) {
        if (!request) {
          return;
        }
        request.abort();
        reject(cancel);
        request = null;
      });
    }
    if (!requestData) {
      requestData = null;
    }
    // 发送 HTTP 请求
    request.send(requestData);
  });
};

代码很长,即使删除了部分代码,依然后150+行,看到这里了就仔细去分析一些吧,快结束了(我也快写不动了😭)
这个文件做了什么呢

  • 返回 Promise
  • 规范了请求字段(data,headers,url等等)
  • 一些方法的边缘探测处理(getAllResponseHeaders
  • 方法校验、状态码,数据流向处理(由status决定),健壮性增强
  • 发起了一个xhr请求

至此,已全部结束主要源码的分析,里面一些工具类函数和辅助函数也不错,也有学习的地方,建议大家看看。
那么我们梳理一些大概的流程

梳理小结

  1. 初始化时创建实例(createinstance),此过程中为实例定义和继承了一些方法 取消请求(CancelCancelTokenisCancel )/错误处理( isAxiosError )/ 请求方法( request )/请求语法糖(get , post , delete 等等)
  2. 将拦截器里的回调(request_fullfilledrequest_rejected, response_fullfilled, response_rejected)装入执行链 chain。有点像Promise的初始化
  3. 调用 disapatchRequest ,利用适配器 Adapter 发起请求。
  4. 对请求数据进行转换(xhr初始化),请求拦截器(如果存在)请求配置合并,并返回配置。
  5. 发出请求( dispactchRequest ),将响应数据返回给响应拦截器(如果存在)。这里存在对数据的二次加工(validateSatatus 区分是否成功,data 不同,失败/成功回调函数接=接受的参数)
  6. 响应拦截器(如果存在)对数据进行二次加工。返回给axios.then() 的回调函数的参数。
  7. END!

⚾带着问题加深理解

1.取消请求如何实现

先看看怎么用,有两种方式。
可以使用 CancelToken.source 工厂方法创建 cancel token,像这样:

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.');

还可以通过传递一个 executor 函数到 CancelToken 的构造函数来创建 cancel token

var CancelToken = axios.CancelToken;
var cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// 取消请求
cancel();

那种更优雅,用着更方便呢,个人认为时第二种。理解直观,使用优雅 (代码少)😂

取消实现主要执行了传入执行器 executor 的参数函数(cancel)

接着看看源码,前面在分析适配器时注释里提了一下,当中有涉及到相关代码

🧾adapters/xhr.js

// 代码很好理解,如果配置了cancelToken 那么就执行 request.abort() 取消请求
if (config.cancelToken) {
 config.cancelToken.promise.then(function onCanceled(cancel) {
 	// 在request 为null时, 直接返回,取消没有意义 
   if (!request) {
     return;
   }

   request.abort();
   reject(cancel);// Promise状态更改为rejected,并将cancel作为错误捕获回调参数!
   // Clean up request
   request = null;
 });
}

上面的 reject(cancel)cancel 是什么?接着来看 lib/cancel 文件夹

🧾cancel/Cancel.js

function Cancel(message) {
  this.message = message;
}
// 重写toString方法
Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

🧾cancel/CancelToken.js

var Cancel = require('./Cancel');
//cancelTOken 接受一个执行器 executor 函数作为参数。 
function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
  	// 将Promise的执行权暴露出去
    resolvePromise = resolve;
  });

  var token = this;
  // 传入的cancel函数触发cancelToken.promise.then   见xhr.js
  executor(function cancel(message) {
    if (token.reason) { // 已经取消过
      return;
    }

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

// 如果请求过了(完成)就把 Cancel 当错误抛出。
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

// 这个不过是上面第一种方式的补充实现。
CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,
    cancel: cancel
  };
};

cancel 其实就是 token.reason !
总结:

  • 传入配置项 cancelToken 一个函数(executor),此函数又接受一个参数(cancel, 也为函数),才参数暴露出去给外部调用(一旦调用, 就执行 request.abort())。
  • 上面 executor(function cancel(){}) 里面的 cancel 函数就是调用方式第二种的 c, 当 c 调用才会触发 promise.then()的回调函数(第一个).
  • axios.get().then().catch(msg => {}) 当发生请求取消,取msg就会调用 Cancel.prototype.toString 方法

由不住再想说一句
卧槽!还可以这样写,绝啊!

2.拦截器如何实现的

其实上面拦截器也说的差不多了,这里做个总结吧
主要代码就是 🧾Core/Axios.js 里面一段

var chain = [dispatchRequest, undefined];
var promise = Promise.resolve(config);

//调用拦截器自定义方法forEach,参考后面[拦截器]
this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
  // interceptor 就是我们传入拦截器的两个函数,  然后插入chain数组开头
  chain.unshift(interceptor.fulfilled, interceptor.rejected);
});

this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
  // 向chain数组末尾添加元素
  chain.push(interceptor.fulfilled, interceptor.rejected);
});

//while循环一步步利用then链向下执行
while (chain.length) {
  promise = promise.then(chain.shift(), chain.shift());
  /*
    相当于 
    promise.then(request_fullfilled, request_rejected)
      .then(dispatchRequestFunc, undefined)
      .then(response_fullfilled, response_rejected)
  */
}

总结:
保持这样一个数组 ,请求拦截器的参数始终向头部添加,响应拦截器的参数始终想尾部添加!

[...,请求拦截器成功函数, 请求拦截器失败函数, XHR请求, undefined,响应拦截器成功函数, 响应拦截器失败函数,...]

然后 用 Promise.then 一个个串起来执行。请求拦截器的配置始终能更改XHR请求, XHR请求的数据也始终会返回到响应拦截器的参数(成功/失败函数)里。这也就解释了

  • 请求拦截器的成功函数里返回 Promise 会挂起请求
  • 响应拦截器的成功函数里放回 Promise 请求不会有响应值。

大概这个样 | 【掘金】逐行解析Axios源码⇲
在这里插入图片描述

3.请求配置的优先级

大家知道,core/defaults.js 里面有配置,axios.create(config) 也有配置, 响应拦截器里也有配置,那么这些配置的优先级是如何的呢?来看看那些地方执行了配置合并
🧾core/Axios.js

Axios.prototype.request = function request(config) {
	config = mergeConfig(this.defaults, config);
}

// 没看到调用
Axios.prototype.getUri = function getUri(config) {
  config = mergeConfig(this.defaults, config);
};

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
	return this.request(mergeConfig(config, ...));
}

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
	his.request(mergeConfig(config, ...));
}

🧾axios.js

axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};

这里我就直接出结论了哈, 优先级由小到大
defaults里config << 初始化config(axios.create(cfg)/axios.get(cfg)) << 请求拦截器参数返回的config.
其实 headers 自定义配置也可以在这里解释!就是一个合并的事

4.请求超时如何实现

当请求时间过程没有响应,超出我们设定的时间(ms),并且进入then
xhr.js

var request = new XMLHttpRequest();

request.timeout = config.timeout;

// 当进度由于预定时间到期而终止时,会触发timeout 事件。
request.ontimeout = function handleTimeout() {
  var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
  if (config.timeoutErrorMessage) {
    timeoutErrorMessage = config.timeoutErrorMessage;
  }
  reject(createError(timeoutErrorMessage, config, 'ECONNABORTED',
    request));
  request = null;
};

其实就是重写 ontimeout 方法,重置 request。 而 timeoutXMLHttpRequest 对象的 原生方法⇲

待补充

后面发现了再去看吧,也希望各位看客补充!

最后

⚾总结

一图总览 【掘金】学习 axios 源码整体架构,打造属于自己的请求库⇲
在这里插入图片描述
— 图是什么画的?
—了解一下 xmind

参考

除了上面提到的 链接,还参考了如下文章。站在别人的肩膀上可以看的更高。
Axios 源码解析
深入浅出 axios 源码
如何实现一个HTTP请求库——axios源码阅读与分析
Axios源码分析

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值