【axios源码】- 工具函数utils研读解析

804 阅读6分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

首发于我的公众号「前端面壁者」,欢迎关注。

一、环境准备

  • axios 版本 v0.24.0

  • 通过 github1s 网页可以 查看 axios源码

  • 调试需要 clone 到本地

git clone https://github.com/axios/axios.git

cd axios

npm start

http://localhost:3000/

二、函数研读

utils is a library of generic helper functions non-specific to axios

utils 是一个非特定于 axios 的通用辅助函数库

1. helper 函数

var bind = require('./helpers/bind');

===>
'use strict';

module.exports = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
};

image.png

Tips: 这是一个配合后述extend工具函数使用的方法,作用是当运行时存在明确的this指向即thisArg存在时将fn扩展(添加)到目标对象中。如图中的showInfo()较之于Object

2. 使用toString()获取对象类型

可以通过 toString() 来获取每个对象的类型,称之为 toStringTag (准确的说应该是Symbol.toStringTag),关于 toString 更多的性质,详情见MDN - toString

【2.1】 isArray

var toString = Object.prototype.toString;
/**
 * 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 toString.call(val) === "[object Array]";
}
  • isArray 封装了 Object 原型链函数 toString(),借助 toString()判断属性类型的性质判断 val 是否为数组
  • Object 原型链函数 toString()在成功判断数组时固定返回'[object Array]'
  • 关于 Array 类型判断详情见ECMA-262 - Let isArray be IsArray(Object)

Tips: 从ECMA-262文档可以看出,从 es6 后 toString()在判定 Array 类型时直接使用了 IsArray 方法,所以如果环境允许,直接使用 Array.isArray()也是可行的 🐶,这点在MDN - Array.isArray的 Polyfill 中也是有体现的。

Tips: Array.isArray() 是 es5 特性

【2.2】 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]";
}

Tips:许多内置的 JavaScript 对象类型即便没有 toStringTag 属性,也能被 toString() 方法识别并返回特定的类型标签,比如:Object.prototype.toString.call([1, 2]); // "[object Array]",但是有些对象类型则不然,toString() 方法能识别它们是因为 引擎 为它们设置好了 toStringTag 标签,比如:Object.prototype.toString.call(new Map()); // "[object Map]",当前的 [object ArrayBuffer] 以及下面的 [object File][object Blob] 都属于这种情形。

【2.3】 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]";
}
  • isArray 封装了 Object 原型链函数 toString(),借助 toString()判断属性类型的性质判断 val 是否为日期类型
  • Object 原型链函数 toString()在成功判断数组时固定返回'[object Date]'
  • 关于 Date 类型判断详情见ECMA-262 - if Object has a [[DateValue]] internal slot, let builtinTag be "Date"

【2.4】 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]";
}
  • 参见 isArrayBufferTips

【2.5】 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]";
}
  • 参见 isArrayBufferTips

【2.6】 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]";
}
  • isArray 封装了 Object 原型链函数 toString(),借助 toString()判断属性类型的性质判断 val 是否为Function类型
  • Object 原型链函数 toString()在成功判断数组时固定返回'[object Array]'
  • 关于 Function 类型判断详情见ECMA-262 - if Object has a [[Call]] internal method, let builtinTag be "Function".

3. 使用 typeof 获取未经计算的操作数

可以通过 typeof 来获取未经计算的操作数的类型,关于 typeof 更多的性质,详情见MDN - typeof

【3.1】 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";
}

【3.2】 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";
}

【3.3】 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";
}

【3.4】 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";
}

_.isObject({});
// => true

_.isObject([1, 2, 3]);
// => true

_.isObject(_.noop);
// => true

_.isObject(null);
// => false
  • 检查 value 是否是普通对象,即排除掉 null 类型的所有对象类型,包含 array、date 等对象类型

4. 使用 instanceof 检测当前实例原型链是否包含 prototype 属性

可以通过 instanceof 运算符检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,关于 instanceof 更多的性质,详情见MDN - instanceof

【4.1】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 (
        typeof URLSearchParams !== "undefined" && val instanceof URLSearchParams
    );
}
  • 关于 isURLSearchParams( URL 的查询字符串 ) 更多的性质,详情见MDN - URLSearchParams

5. 复合类型

通过 toString()typeofinstanceof等 API 及上述工具函数功能组合

【5.1】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)
    );
}
  • 先判断不是 undefined 和 null,再判断 val存在构造函数,因为Buffer本身是一个类,最后通过自身的isBuffer方法判断
  • axios 可以运行在浏览器和 node 环境中,所以内部会用到 nodejs 相关的 Buffer 类知识。
  • 关于 Buffer 更多的性质,详情见nodejs - Buffer

【5.2】 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;
}
  • 先判断不是 undefined 和 再判断 ArrayBuffer 原型链存在isView方法,如果原型链存在 isView 方法,则使用ArrayBuffer.isView()判断,否则调用上述封装的isArrayBuffer()方法
  • 关于 ArrayBuffer 更多的性质,详情见MDN - ArrayBuffer

【5.3】 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;
}

function Foo() {
    this.a = 1;
}

_.isPlainObject(new Foo());
// => false

_.isPlainObject([1, 2, 3]);
// => false

_.isPlainObject({ x: 0, y: 0 });
// => true

_.isPlainObject(Object.create(null));
// => true
  • 判断目标对象的原型是不是nullObject.prototype
  • 顾名思义,目标对象在原型链顶端或者其原型为 Object 类型,是纯粹的对象

【5.4】 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 typeof FormData !== "undefined" && val instanceof FormData;
}

【5.5】 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);
}

6. 正则表达式

【6.1】 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, "");
}

7. 重写

【7.1】 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);
            }
        }
    }
}
  • 如果是 null 或 undefined 直接返回
  • 如果不可遍历则转换成 Array
  • 如果是 Array 类型直接循环遍历
  • 如果是 Object 类型则使用原型链上的hasOwnProperty()方法遍历,值得注意的是不推荐使用对象实例上的hasOwnProperty(),关于这一点参考eslint no-prototype-builtins

Tips: 这是一个配合后述mergeextend工具函数的方法,因此 call 中必填项thisArg未设置指向

8. js strikes

【8.1】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;
}
  • 作用是合并有相同键的 val 值

  • 配合前述forEach工具函数,根据arguments长度循环遍历其中的每一项,需要注意的是arguments[i]有可能是对象 Object、数组 Array、null 或者未定义 undefined

  • 首先进入 for 循环,其中 arguments 是一个对应于传递给函数的参数的类数组对象

  • 每次遍历时调用assignValue方法,入参为forEach工具函数返回的内容

  • 第一层 if 判断result中是否有键为key的纯对象,并且forEach返回的valobj[key] 是否是纯对象,满足条件则进入递归,将 val 合并至 result 对应的键key

  • 第二层 else if result中是不含键为key的纯对象,并且 val 值是纯对象,满足条件则进入递归,在result中新建一个键为key的,值为val的纯对象

  • 第三层 else if val 值是数组,满足条件则进入递归,在result中新建一个键为key的,值为val的对象,其中val.slice()作用是拷贝数据

Tips:在对Object类型的可枚举属性的处理上可以参考MDN - Object.assign 以及 阮一峰 - e6入门merge相对Object.assign主要是解决浅拷贝的问题。

【8.2】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;
}
  • 作用是将Object b的属性和方法添加到Object a
  • 如果待添加的valfunction且运行时存在明确的this指向thisArg,需要通过调用bind绑定至当前对象即a
  • 如果待添加的valproperty,直接在a上添加相应的key-val键值对

【8.3】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;
}
  • 去掉字节顺序标记 BOM
  • 字节顺序标记(英语:byte-order mark,BOM)是位于码点 U+FEFF 的统一码字符的名称。当以 UTF-16 或 UTF-32 来将 UCS/统一码字符所组成的字符串编码时,这个字符被用来标示其字节序,更多内容可以参考MDN - TextDecoder

9 环境

【9.1】 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";
}

三、参考

1. Ethan01的文章阅读axios源码,发现了这些实用的基础工具函数

2. 李冰老师的专栏图解 Google V8 - 一篇文章彻底搞懂JavaScript的函数特点

3. MDN