Axios 源码分析(二): Axios 工具函数

Axios 源码分析(二): Axios 工具函数

Cocytus Elias 94 2023-04-26

本文参加了每周一起学习200行源码共读活动

Axios 的工具函数位于lib/utils点此前往

isArray

/**
 * Determine if a value is an Array
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an Array, otherwise false
 */
function isArray(val) {
  return Array.isArray(val);
}

isArray 函数主要是判断是否是数组。

判断数组一般有以下几种方法:

type of

这是最笨,也是很不优雅的方法。首先使用 type of 判断是否是 Object,之后再判断有没有 length 这个属性。

function isArray(val) {
    return typeof val === "object" && val.hasOwnProperty("length");
}

这个方法最大的缺陷是,不管定义任何类或者 object,它的对象都是 object,如果定义的类或者 object 中带有 length 属性。则使用这个检测方法也会返回 true

instanceof

Instanceof 是一种运算符,主要用来判断 A 是否是 B 的实例。其基本原理是当 A__proto__ 指向 Bprototype 时,就认为 A 就是 B 的实例。

如下所示:任意两个 array__proto__ 是相等的,并且任意一个 array__proto__Array.prototype 也是相等的。

let a = [];
let b = [1,2,3];
console.log(a.__proto__ === b.__proto__); // true
console.log(a.__proto__ === Array.prototype); // true
console.log(a instanceof Array); // true

所以可以实现如下代码:

function isArray(val) {
    return val instanceof Array;
}

instanceof 的主要问题是,无法判断使用 ifram 生成的。

constructor

所有实例都会从类上继承一个 constructor 属性。使用 constructor 与 instanceof 有异曲同工之妙,当然问题也是一样的。

代码如下:

function isArray(val) {
    return val.constructor === Array;
}

Object.prototype.toString.call()

这个方法可以说是一个比较好的方法,能够准确的判断出是什么类型。但是它的性能相对来说要差很多。

function isArray(val) {
    return Object.prototype.toString.call(val) === "[object Array]";
}

Array.isArray

这是 Array 的内置方法,与Object.prototype.toString.call() 差不多,但是性能要比 Object.prototype.toString.call() 好,而这个也是 axios 官方在使用的方法。代码如下:

function isArray(val) {
  return Array.isArray(val);
}

isUndefined

/**
 * Determine if a value is undefined
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if the value is undefined, otherwise false
 */
function isUndefined(val) {
  return typeof val === 'undefined';
}

isUndefined 函数主要是判断是否是 undefined

这个十分简单,不过在判断 undefined 方面也有多种方法。

取反

直接使用取反可以判断 undefined ,但是需要注意的是,取反是一个隐式转换,即先转为 boolean 判断。所以如果使用取反,则 undefined null false 0 "" 都会生效 需要对 undefined 进行单独区分的时候,这个就没有作用了。

===

可以使用强等于来判断是否时 undefined,注意一定是强 === 。因为如果使用弱等于 ==,则无法分辨出 nullundefind

在这里需要提一嘴 nullundefined 的区别,在 js 的最初版本中,只有 null ,但是后来由于种种原因,又设置了 undefined ,并标明:

  • undefind 用来代表变量的最初始值,即应该有这个变量或属性,但是并未定义。这一点可以从声明一个变量并打印的结果 ( undefined ) 、直接使用一个未声明的变量的异常 (xxx is not defined) 、函数直接 return; 或者不写 return 的返回值 ( undefined ) ,调用对象不存在的属性的结果 ( undefined ) 的来看出。
  • null 用来代表一个空对象,或者可以说是一个空指针,以此来代表没有相应的对象。通俗点说就是,栈中的变量没有指向堆中的内存对象。所以一般使用 xx = null 来释放内存对象。这个从typeof null的结果 ( object ) 上就可以看出来,显然 js 认为 null 是一个对象,只不过是引用为空的对象。

以此来看,其实 nullundefind 相差不大,有也只是一些细微的差别(主要是语义上)。

typeof

这个是官方的使用方法,这个是比较省事也比较准确的一种方法。

Object.prototype.toString.call()

这是直接调用了原型,输出对象的类型 '[object Undefined]'。这个方法主要是用来判断各种的内置类型,并且是最准的,所以不仅仅是 Undefined ,包括上面的 ArrayObject、浏览器的 Window 都可以。

isBuffer

/**
 * Determine if a value is a Buffer
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Buffer, otherwise false
 */
function isBuffer(val) {
  return val !== null && !isUndefined(val) && val.constructor !== null && !isUndefined(val.constructor)
    && typeof val.constructor.isBuffer === 'function' && val.constructor.isBuffer(val);
}

这个是判断是否是 buffer 对象。

因为 js 自身是不能很好的处理二进制数据类型的,这在平常使用中没有太大问题,但是一旦碰到流式处理的场景,就显得捉襟见肘。因此在 Node.js中,定义了一个Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。

但是 buffer 既不是单纯的流,也不是单纯的二进制,而是一个缓冲区使用流的形式处理二进制数据。不准确的说,可以将 buffer 对象理解为 go 中的通道或者是消息队列,也可以看做是一种滑动窗口。

buffer 对象占用的内存空间是不计算在 node 进程内存空间限制上的,即它的内存,分配在 V8 堆栈外。但是 buffer 空间也有上限,并且这个上限是固定的,如果向 buffer 中存入的数据到达上限则不能再存入,需要先取后存。这种其实就是一种流的形式,所以 buffer 其实就是一个缓冲区。

官方的方法主要是通过判断:非空数据 + 对象构造函数的 isBuffer 是否是 function

当然这个方法并不是很准确,比如我使用如下的方法伪造一个 buffer 就能通过检测。

let fakeBuffer = {
    constructor: {
        isBuffer: function(val) {
            return true;
        }
    }
}

isBuffer(fakeBuffer) // true

node 环境中可以使用 toString.call(val) === [object Uint8Array],但是在浏览器环境下并不支持 buffer

isArrayBuffer

/**
 * Determine if a value is an ArrayBuffer
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an ArrayBuffer, otherwise false
 */
function isArrayBuffer(val) {
  return toString.call(val) === '[object ArrayBuffer]';
}

检查是否是 ArrayBuffer 对象。

ArrayBuffer 对象是一个通用的、固定长度的原始二进制数据缓冲区,数据结构为字节数组,它不能直接读写,只能通过视图进行操作。

它可以通过两种视图进行操作:TypedArray (简单类型的二进制数据读写) 以及 DataView (复杂类型的二进制数据读写) 。而 buffer 其实是 TypedArray 的一种实现。

所以,ArrayBuffer 是一个基础、原始的缓冲区实现,所以 ArrayBuffer 也是一个内置对象。

isFormData

/**
 * Determine if a value is a FormData
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an FormData, otherwise false
 */
function isFormData(val) {
  return toString.call(val) === '[object FormData]';
}

检查是否是 FormData 对象,FormData 对象可以使用 append k/v 的方式生成一个表单数据。

toString.call()Object.prototype.toString.call() 的简写形式。就像是浏览器中 Websocketwindows.Websocket 的简写形式一样。

当然,这里官方其实做了下变量声明,在 utils.js 的头部声明了一个 var toString = Object.prototype.toString;,但其实直接使用 toString.call() 也是可以的。

因为 FormData 属于内置对象,所以可以直接使用 toString.call() 的方法进行检测。

isArrayBufferView

/**
 * Determine if a value is a view on an ArrayBuffer
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a view on an ArrayBuffer, otherwise false
 */
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;
}

判断是否是 ArrayBuffer 的视图。

isString

/**
 * Determine if a value is a String
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a String, otherwise false
 */
function isString(val) {
  return typeof val === 'string';
}

判断是否是字符串,最简单的方法就是 typeof,也可以使用 Object.prototype.toString.call()

当然在做字符串判断时还需要注意一点,是否是非空的字符串,这一块在实际使用中根据自己需求来即可。

isNumber

/**
 * Determine if a value is a Number
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Number, otherwise false
 */
function isNumber(val) {
  return typeof val === 'number';
}

判断是否是数字,官方的这个方法可以用来做简单的数字判断,除此之外,也有可用于其他特殊场景的一些判断方法。详见数字判断工具函数

isObject

/**
 * Determine if a value is an Object
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an Object, otherwise false
 */
function isObject(val) {
  return val !== null && typeof val === 'object';
}

判断是否是 object 。需要注意的是,不论是 arraymapjson、浏览器中的 window,他们的 typeof 都是 object。如果想判断是否是 json,较好的方法是使用 Object.prototype.toString.call() ,但是一些,当然下面的 isPlainObject 也不失为一种好方法。

isPlainObject

/**
 * Determine if a value is a plain Object
 *
 * @param {Object} val The value to test
 * @return {boolean} True if value is a plain Object, otherwise false
 */
function isPlainObject(val) {
  if (toString.call(val) !== '[object Object]') {
    return false;
  }

  var prototype = Object.getPrototypeOf(val);
  return prototype === null || prototype === Object.prototype;
}

这个是用来判断一个非官方标准定义的 object - PlainObject

PlainObject 指的是一个纯粹的对象,即 js 中原生的 {} 或使用 new object 创建的对象。

关于这个检测方法的详细解释可以看这篇提问的高赞回答

isDate

/**
 * Determine if a value is a Date
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Date, otherwise false
 */
function isDate(val) {
  return toString.call(val) === '[object Date]';
}

用来判断是否是一个 Date 对象。

isFile

/**
 * Determine if a value is a File
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a File, otherwise false
 */
function isFile(val) {
  return toString.call(val) === '[object File]';
}

判断是否是一个文件对象,一般是用在文件上传上。

isBlob

/**
 * Determine if a value is a Blob
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Blob, otherwise false
 */
function isBlob(val) {
  return toString.call(val) === '[object Blob]';
}

判断是否是一个类文件对象。

isFunction

/**
 * Determine if a value is a Function
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Function, otherwise false
 */
function isFunction(val) {
  return toString.call(val) === '[object Function]';
}

判断是否是函数。

isStream

/**
 * Determine if a value is a Stream
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a Stream, otherwise false
 */
function isStream(val) {
  return isObject(val) && isFunction(val.pipe);
}

判断是否是流。主要是通过是否存在指定方法来判断的。 因为 stream 本身的原型是一个 objectstream 自身更是一个 function

isURLSearchParams

/**
 * Determine if a value is a URLSearchParams object
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is a URLSearchParams object, otherwise false
 */
function isURLSearchParams(val) {
  return toString.call(val) === '[object URLSearchParams]';
}

判断是否是一个 URL 的查询字符串对象。

URLSearchParams 是一个原生 api 。使用此 api 可以快速生成一个 get urlparams 字符串,而不需要去手动拼接。

trim

/**
 * Trim excess whitespace off the beginning and end of a string
 *
 * @param {String} str The String to trim
 * @returns {String} The String freed of excess whitespace
 */
function trim(str) {
  return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
}

去除前后空格。这里进行了一下判断,如果在一些没有 str.trim 的浏览器中,使用后面的正则替换方法来删除空字符。

isStandardBrowserEnv

/**
 * 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'
 */
function isStandardBrowserEnv() {
  if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' ||
                                           navigator.product === 'NativeScript' ||
                                           navigator.product === 'NS')) {
    return false;
  }
  return (
    typeof window !== 'undefined' &&
    typeof document !== 'undefined'
  );
}

判断是否是标准浏览器环境,对于 isStandardBrowserEnv ,首先我们先看下 isStandardBrowserEnv 的判断方法:使用 navigatorproduct 属性 和 windowdocument 是否为空。

对此我们会有几个疑惑:

  • navigator 是一个不推荐的对象,原因是很有可能会被浏览器使用者修改。但是这个方法只是用来做前置处理,即使被修改了,影响的也只是一些参数。并且对于一些主流浏览器来说,修改 navigator 也并非易事。
  • windowdocument 理论上也可以被修改或删除。这一块可以这么理解,一个是对于一些主流浏览器来说,修改这种浏览器原生对象并非易事,另一个是 windowdocument 都已经不存在了,代表网页也可能挂掉了,这个时候对于正常的用户来说,肯定是无法使用,那判断准不准确也没什么关系。

因为 axios 既可以在浏览器中发送请求,又可以在 node 环境中使用,所以肯定需要判定是在什么环境下。但是这里的 isStandardBrowserEnv 其实并不是为了判断使用什么方式发送请求的,而是为了做一些前置处理,比如设置一些 headercookie,做一些同源策略判断等等。

Axios 在发送请求前做了另一种判断,可以在 /lib/defaults 下的 getDefaultAdapter 方法中看到(如下)。根据当前是否有 XMLHttpRequestprocess 来判断是标准浏览器还是 node 环境。如果两者皆不是则直接返回一个空的,就无法进行调用了。

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

forEach

/**
 * Iterate over an Array or an Object invoking a function for each item.
 *
 * If `obj` is an Array callback will be called passing
 * the value, index, and complete array for each item.
 *
 * If 'obj' is an Object callback will be called passing
 * the value, key, and complete object for each property.
 *
 * @param {Object|Array} obj The object to iterate
 * @param {Function} fn The callback to invoke for each item
 */
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);
      }
    }
  }
}

这是 axios 自己实现的一个增强型 forEach。逻辑很简单:

  • 如果不是对象,直接包装为数组,并按数组的方式来使用。
  • 如果是数组,遍历数组中的每一个元素,并将元素、下标、数组本身按顺序传给一开始传入的 fn 处理函数。
  • 如果是 json ,遍历 json 中的每一对元素,并将 vkjson 本身按顺序传给一开始传入的 fn 处理函数。

原生的 forEach 只能遍历数组,json 需要使用 Object.keys 拍扁后进行遍历。而这个增强型数组则可以直接遍历 arrayjson,而不需要关注具体的对象到底是 json 还是 array

因为 axios 需要对 headers ( json 格式 )、请求 method ( array 格式 )、拦截器 ( array 格式 ) 等对象做遍历处理,这样一来就能精简很多代码,并且也能统一遍历的使用方法。

merge

/**
 * Accepts varargs expecting each argument to be an object, then
 * immutably merges the properties of each object and returns result.
 *
 * When multiple objects contain the same key the later object in
 * the arguments list will take precedence.
 *
 * Example:
 *
 * ```js
 * var result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // outputs 456
 * ```
 *
 * @param {Object} obj1 Object to merge
 * @returns {Object} Result of all merge properties
 */
function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  function assignValue(val, key) {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      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;
}

这个方法是用来合并配置信息用的,比如使用 axios 设置了多次配置,最后配置是从后向前合并的,就是用了这个方法。

这个函数设计的思路比较精妙,首先不断的分解问题(递归),分解到最低的时候对相应的 k v 或对象进行合并处理并提交至顶层的 result 中。

js 原生有一个合并方法 object.assign。但是这个方法有一个缺陷,对于元素,只要存在都是直接覆盖掉。比如:

// 有这样两个对象,我想将他们合并起来。
let test1 = {"info":{"name":"elaissama","age":18},id: 3}
let test2 = {"info":{"name":"elais"},id: 5}

// 我的期望值是
let result = {"info":{"name":"elias","age":18},id: 5}
// 但是,如果使用了 object.assign ,结果就成了
result = {"info":{"name":"elias"},id: 5}


在这种情况下,object.assign 是无法使用的,所以就需要使用上面的方法。

extend

/**
 * Extends object a by mutably adding to it the properties of object b.
 *
 * @param {Object} a The object to be extended
 * @param {Object} b The object to copy properties from
 * @param {Object} thisArg The object to bind function to
 * @return {Object} The resulting value of object a
 */
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

这是一个继承方法,目的是将 B 对象的一些属性和方法拷贝到 A 对象中。在上一篇文章中,我们的图例标明了,最外层暴露给我们用的 axios 其实是使用这个方法继承了内部的 Axios 类上的方法和属性。

stripBOM

/**
 * Remove byte order marker. This catches EF BB BF (the UTF-8 BOM)
 *
 * @param {string} content with BOM
 * @return {string} content value without BOM
 */
function stripBOM(content) {
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
}

这个方法是用来删除字节序标识符。

字节序标识符主要是用来告诉计算机的字节读取顺序,常被用来当做标示以UTF-8UTF-16UTF-32为编码的文件。

之所以要删除这个字节序标识符,是因为 UTF-8 的编码方式不同,不使用字节序标识符也可以正确读取数据,而且在一些特定情境下还会导致解析出错。