源码学习——axios中的工具函数

78 阅读7分钟

序言

学习源码,可能里面的很多功能逻辑在日常中很少用到,但是其代码的组织形式,以及其中的工具函数,却是在日常开发中能够真正实践使用的。尤其是工具函数,无论什么项目都是不可或缺的。本篇文章就来学习下axios中的工具函数。

源码学习

源码地址:github.com/axios/axios… 版本是0.27.2

我们要学的uitls.js文件放在lib文件夹下。 看最后导出的函数:其中大部分都是判断数据类型的辅助函数,由于axios是支持node环境下的请求的,因此在里面我们可以看到像isArrayBufferisBufferisArrayBufferViewisFileisBlobisStream等文件和二进制数据的类型判断。

module.exports = {
  isArray: isArray,
  isArrayBuffer: isArrayBuffer,
  isBuffer: isBuffer,
  isFormData: isFormData,
  isArrayBufferView: isArrayBufferView,
  isString: isString,
  isNumber: isNumber,
  isObject: isObject,
  isPlainObject: isPlainObject,
  isUndefined: isUndefined,
  isDate: isDate,
  isFile: isFile,
  isBlob: isBlob,
  isFunction: isFunction,
  isStream: isStream,
  isURLSearchParams: isURLSearchParams,
  isStandardBrowserEnv: isStandardBrowserEnv,
  forEach: forEach,
  merge: merge,
  extend: extend,
  trim: trim,
  stripBOM: stripBOM,
  inherits: inherits,
  toFlatObject: toFlatObject,
  kindOf: kindOf,
  kindOfTest: kindOfTest,
  endsWith: endsWith,
  toArray: toArray,
  isTypedArray: isTypedArray,
  isFileList: isFileList,
  forEachEntry: forEachEntry,
  matchAll: matchAll,
  isHTMLForm: isHTMLForm,
  hasOwnProperty: hasOwnProperty
};

kindOfkindOfTest方法的学习

利用闭包缓存数据,减少数据的冗余操作。我理解是一种工厂函数,批量生产对应的函数。

var toString = Object.prototype.toString;
var kindOf = (function(cache) {
  return function(thing) {
    var str = toString.call(thing);
    return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());
  };
})(Object.create(null));

function kindOfTest(type) {
  type = type.toLowerCase();
  return function isKindOf(thing) {
    return kindOf(thing) === type;
  };
}

kindOf方法 获取数据的类型

  • 利用自执行函数,生成一个闭包,保存缓存的数据。返回一个函数,用来判断传进来的类型。
  • 判断类型用的是Object.prototype.toString方法。
  • 利用闭包内的cache来缓存我们的判断结果,以使当传进来相同参数时,不用重复的进行判断逻辑,直接拿结果
  • Object.create(null) 创建一个没有原型的对象,防止原型链上存在相同属性的情况。
  • str.slice(8, -1).toLowerCase(),利用字符串截取,因为toString()返回的结果是有固定格式的[object xxxx]xxxx的首字母是大写的。为了简化传参判断,统一用小写来判断。
    • 这里简单说下str.slice(8, -1),由于返回的是固定格式的,所以我们的截取范围就是(8, str.length-1],前开后闭。当slice的第二个参数是负数是,会被计算为length+传进来的负值,这样我们就可以截取出我们想要的类型了。

kindOfTest(type) 返回一个判断是否符合某种类型的函数

这个就是一个工厂函数,根据传进来的type返回一个判断该type的函数。

源码中利用这个工厂函数,生成的类型判断函数。

var isArrayBuffer = kindOfTest('ArrayBuffer');
var isDate = kindOfTest('Date');
var isFile = kindOfTest('File');
var isBlob = kindOfTest('Blob');
var isFileList = kindOfTest('FileList');
var isURLSearchParams = kindOfTest('URLSearchParams');
var isHTMLForm = kindOfTest('HTMLFormElement');

除了上面这些可以直接通过toString.call()来进行判断,还有一些其他的类型需要特殊的处理。

其他类型的判断

isBuffer 判断是否是buffer

Buffer这个类本身提供判断是否是buffer的方法,也就是Buffer.isBuffer。这里看下源码,实现的更严谨些。

function isBuffer(val) {
  return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor)
    && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val);
}
  • 先判断是非null和非undefined
  • 在判断其是一个构造函数
  • 然后利用构造函数本身的isBuffer方法来判断

什么是Buffer? Javacript语言没有用于读取或操作二进制数据流的机制。Buffer类是作为Node.js API的一部分引入的,用于在TCP流、文件系统操作、以及其他上下文中与八位字节流进行交互。总结来说就是该类用来创建一个专门存放二进制数据的缓存区,可以使Node.js用来处理二进制流数据或者与之进行交互。

isArrayBufferView

用来判断传入的参数值是否是一种ArrayBuffer视图(view)。

function isArrayBufferView(val) {
  var result;
  if ((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)) {
    result = ArrayBuffer.isView(val);
  } else {
    result = (val) && (val.buffer) && (isArrayBuffer(val.buffer));
  }
  return result;
}

isStream

判断一个值是一个Stream

function isStream(val) {
  return isObject(val) && isFunction(val.pipe);
}
  • 值是一个对象,并且其pipe属性是个函数,则可以判断该值是一个stream

isFormData

判断是否是一个FormData。

function isFormData(thing) {
  var pattern = '[object FormData]';
  return thing && (
    (typeof FormData === 'function' && thing instanceof FormData) ||
    toString.call(thing) === pattern ||
    (isFunction(thing.toString) && thing.toString() === pattern)
  );
}

FormData对象其实是在XMLHttpRequest2级定义的,定义它的目的就是为了序列化表单以及创建与表单格式相同的数据提供遍历。

这个函数用来三种方式来判断传入的值是否是一个FormData。让我们来看下其依据是什么?

我们找到这个方法的issue可以看到: 主要是为了兼容1、对使用config.env.FormData选项重载FormData的支持。2、通过form-data包增加对node.js环境中FormData的支持。

isStandardBrowserEnv 判断标准浏览器环境

function isStandardBrowserEnv() {
  var product;
  if (typeof navigator !== 'undefined' && (
    (product = navigator.product) === 'ReactNative' ||
    product === 'NativeScript' ||
    product === 'NS')
  ) {
    return false;
  }

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

主要用来区分web works和 react native环境。两个环境都支持XMLHttpRequest,但在react native中不支持完全标准的全局变量。因此用以区分。

forEach 遍历对象或数组

function forEach(obj, fn) {
  // Don't bother if no value provided
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // Force an array if not already something iterable
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    obj = [obj];
  }

  if (isArray(obj)) {
    // Iterate over array values
    for (var i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // Iterate over object keys
    for (var key in obj) {
      if (Object.prototype.hasOwnProperty.call(obj, key)) {
        fn.call(null, obj[key], key, obj);
      }
    }
  }
}

这里抽象一个forEach方法,主要是为了方便后续方法中针对数组和对象的遍历。

merge 将传入的对象进行合并。当存在相同key时,后面的会将前面的进行覆盖。

function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      // 如果已经存在相同的key了,且是一个对象,后面的将覆盖前面的
      result[key] = merge(result[key], val);
    } else if (isPlainObject(val)) {
      // 如果值是一个对象,将值重新取出,返回一个新对象
      result[key] = merge({}, val);
    } else if (isArray(val)) {
      // 如果值是一个数组,返回一个浅拷贝的新数组
      result[key] = val.slice();
    } else {
      result[key] = val;
    }
  }

  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

extend 扩展对象,将b对象的属性,扩展到a对象上。

function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      // 这里要考虑对象中的某个属性为函数时,要处理下this
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

toFlatObject 将一个对象连其原型链上的属性,扁平为一个对象

function toFlatObject(sourceObj, destObj, filter, propFilter) {
  var props;
  var i;
  var prop;
  var merged = {};

  destObj = destObj || {};
  // eslint-disable-next-line no-eq-null,eqeqeq
  if (sourceObj == null) return destObj;

  do {
    props = Object.getOwnPropertyNames(sourceObj);
    i = props.length;
    while (i-- > 0) {
      prop = props[i];
      if ((!propFilter || propFilter(prop, sourceObj, destObj)) && !merged[prop]) {
        destObj[prop] = sourceObj[prop];
        merged[prop] = true;
      }
    }
    sourceObj = filter !== false && Object.getPrototypeOf(sourceObj);
  } while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype);

  return destObj;
}

这里一时不清楚使用场景,看了源码中的使用,一个是扁平错误信息,另一个是过滤对象中is开头的方法.

过滤某些符合某些条件的属性,这里可以理解。但是其扁平原型链上的属性还是不清楚,直到我看到测试用例这个例子,就突然理解了在错误处理那块的逻辑。将对象继承的一些属性同时提取出来。

 const a = {x: 1};
 const b = Object.create(a, {y: {value: 2}});
 const c = Object.create(b, {z: {value: 3}});
 toFlatObject(c)

这里第一次在源码中看到do while语法的使用,不论是否循环,必须执行一次。

matchAll 接受一个正则表达式和一个字符串,并返回包含所有匹配项的数组

const matchAll = (regExp, str) => {
  let matches;
  const arr = [];

  while ((matches = regExp.exec(str)) !== null) {
    arr.push(matches);
  }

  return arr;
}

这里可以复习下正则表达式的懒惰性

  • reg.lastIndex: 当前正在下一次匹配的起始索引位置。默认情况下,lastIndex的值不会被改变,每一次都是从字符串开始位置查找,所以永远找到的都是第一个,这就是其懒惰性捕捉的原因。解决办法就是,利用全局修饰符g
  • 当添加了全局优势符g后,第一次匹配玩后,lastIndex会自己修改,这样就可以把整个字符串符合匹配项的都捕获到。

可以看下源码中的使用:将类似这样的属性引用 foo[x][y][z]转变为一个数组。

function parsePropPath(name) {
  // foo[x][y][z]
  // foo.x.y.z
  // foo-x-y-z
  // foo x y z
  return utils.matchAll(/\w+|\[(\w*)]/g, name).map(match => {
    return match[0] === '[]' ? '' : match[1] || match[0];
  });
}

传入foo[x][y][z],最后返回的就是['foo', 'x', 'y', 'z']

收获

  • 类型判断的学习,尤其是kindOfkindOfTest

    • 作为开源库,无时无刻不在体现性能的优化,利用闭包缓存数据,减少冗余的数据操作,提升处理性能。
    • 抽象的能力,如何将通用的逻辑进行抽象。kindOfTest抽象为工厂函数,批量产出类型判断函数,非常的优雅。
  • do while语法的复习,很多时候在业务很难使用的一些语法,在源码中却可以找到对应的场景,还是要跟这些优秀的开源作者学习,都是对JS理解很深刻的大佬。

  • 正则的复习:捕获的懒惰性与贪婪性,简单介绍一下。

    • 懒惰性也就是:每次都是从字符串的开始进行匹配,导致每次匹配的都是一样,解决方案就是加一个全局修饰符g,这样没匹配一次,其下一次的起始位置就会跟着改变
    • 贪婪性也就是:正则的每一次捕获都是按照匹配最长的结果捕获的。解决方案就是在量词元字符后边加个?。
  • 另一个收获就是开源的库是在不断的优化的,不断的加入更优雅的解决方法,参考文章的axios版本还是0.24.0,我看的版本是0.27.2。原来的类型判断函数都是单个判断,而在这一版就加入了工厂函数的实现,利用kindOfkindOfTest批量产出类型判断的方法。