源码解析(二):通用工具方法

506 阅读10分钟

上篇 Axios 源码解析(一):模块分解Axios 工程的结构进行了分解,下面来解析通用工具方法部分的源码,包括包括 utils.js/helpers 目录。

github.com/MageeLin/ax… 中的analysis分支可以看到当前已解析完的文件。

utils.js

utils.js 中包含的方法如下所示:

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,
};

可以发现,七成的方法都是 is 开头的,也就是进行判断的工具方法。这些判断方法很多在 lodash 之类的库中都实现过,甚至 JS 已经原生实现了一部分。但是 Axios 毕竟是个久经考验的库,所以值得学习下如何手动稳定实现:

// 引入绑定this指向的bind方法
var bind = require('./helpers/bind');

// utils 是一个通用的辅助函数库,不特定于axios使用

// 借用Object原型上的toString方法
var toString = Object.prototype.toString;

/**
 * 判断value是否为数组
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是数组返回true,否则返回false
 */
function isArray(val) {
  return toString.call(val) === '[object Array]';
}

/**
 * 判断value是否为undefined
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是undefined返回true,否则返回false
 */
function isUndefined(val) {
  return typeof val === 'undefined';
}

/**
 * 判断value是否为Buffer
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是Buffer返回true,否则返回false
 */
function isBuffer(val) {
  return (
    val !== null &&
    !isUndefined(val) &&
    val.constructor !== null &&
    !isUndefined(val.constructor) &&
    typeof val.constructor.isBuffer === 'function' &&
    val.constructor.isBuffer(val)
  );
}

/**
 * 判断value是否为ArrayBuffer
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是ArrayBuffer返回true,否则返回false
 */
function isArrayBuffer(val) {
  return toString.call(val) === '[object ArrayBuffer]';
}

/**
 * 判断value是否为FormData
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是FormData返回true,否则返回false
 */
function isFormData(val) {
  return typeof FormData !== 'undefined' && val instanceof FormData;
}

/**
 * 判断value是否为ArrayBuffer上的view
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是ArrayBuffer上的view返回true,否则返回false
 */
function isArrayBufferView(val) {
  var result;
  if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView) {
    result = ArrayBuffer.isView(val);
  } else {
    result = val && val.buffer && val.buffer instanceof ArrayBuffer;
  }
  return result;
}

/**
 * 判断value是否为String
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是String返回true,否则返回false
 */
function isString(val) {
  return typeof val === 'string';
}

/**
 * 判断value是否为Number
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是Number返回true,否则返回false
 */
function isNumber(val) {
  return typeof val === 'number';
}

/**
 * 判断value是否为Object
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是Object返回true,否则返回false
 */
function isObject(val) {
  return val !== null && typeof val === 'object';
}

/**
 * 判断value是否为 纯Object
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是 纯Object 返回true,否则返回false
 */
function isPlainObject(val) {
  if (toString.call(val) !== '[object Object]') {
    return false;
  }

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

/**
 * 判断value是否为 Date
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是 Date 返回true,否则返回false
 */
function isDate(val) {
  return toString.call(val) === '[object Date]';
}

/**
 * 判断value是否为 File
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是 File 返回true,否则返回false
 */
function isFile(val) {
  return toString.call(val) === '[object File]';
}

/**
 * 判断value是否为 Blob
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是 Blob 返回true,否则返回false
 */
function isBlob(val) {
  return toString.call(val) === '[object Blob]';
}

/**
 * 判断value是否为 Function
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是 Function 返回true,否则返回false
 */
function isFunction(val) {
  return toString.call(val) === '[object Function]';
}

/**
 * 判断value是否为 Stream
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是 Stream 返回true,否则返回false
 */
function isStream(val) {
  return isObject(val) && isFunction(val.pipe);
}

/**
 * 判断value是否为 URLSearchParams对象
 *
 * @param {Object} val 要检验的值
 * @returns {boolean} 是 URLSearchParams对象 返回true,否则返回false
 */
function isURLSearchParams(val) {
  return (
    typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams
  );
}

/**
 * 判断是否运行在标准浏览器环境中
 *
 * 允许 axios 在浏览器工作者线程和react-native中运行。
 * 两种环境都支持 XMLHttpRequest,但并不是完全标准的全局变量。
 *
 * 浏览器工作者线程:
 *  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';
}

注意: bind 是在 /helpers 中实现的一个方法,作用是手动修改 this 的指向

trim

trimutils.js 中的第一个不以 is 开头的方法。作用很简单,和 String.prototype.trim() 的功能相同,使用正则表达式匹配,将前后的空白剔除掉:

/**
 * 修剪字符串前后的空白
 *
 * @param {String} str 要修剪的字符串
 * @returns {String} 去除前后空白后的字符串
 */
function trim(str) {
  return str.replace(/^\s*/, '').replace(/\s*$/, '');
}

forEach

forEach 是项目中大量使用的一个方法。它既可以迭代纯对象,也可以迭代数组,fn 参数是迭代过程中的回调函数。

/**
 * 迭代数组或对象,对每一子项都执行一个回调函数
 *
 * 如果 `obj` 是一个数组,将会将子项值、索引和整个数组传给回调函数
 * 如果 `obj` 是一个对象,将会将子项值、子项键和整个对象传给回调函数
 *
 * @param {Object|Array} obj 要迭代的对象
 * @param {Function} fn 对每一项执行的回调函数
 */
function forEach(obj, fn) {
  // 如果obj没有值,就直接return
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  // 如果obj不是一个对象,就封装成数组
  if (typeof obj !== 'object') {
    /*eslint no-param-reassign:0*/
    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);
      }
    }
  }
}

个人认为这种实现方式不好,迭代数组和迭代对象的方法应分别封装,便于理解和找错。

merge

merge 是个可变参数的函数,期望每个参数都是一个对象,然后把所有的参数的属性合并,返回合并后的新对象。

内部使用了 arguments 来实现的可变参数,使用递归来将对象进行了深层次拆分:

/**
 * varargs 期望每个参数都是一个对象,然后合并每个对象的属性并返回新结果(原来的对象不可变)。
 *
 * 当多个对象包含相同的键时,参数列表中后面的对象将覆盖之前的。
 *
 * 举例:
 *
 * ```js
 * var result = merge({foo: 123}, {foo: 456});
 * console.log(result.foo); // 输出 456
 * ```
 *
 * @param {Object} obj1 要合并的对象
 * @returns {Object} 合并所有属性后的结果
 */
function merge(/* obj1, obj2, obj3, ... */) {
  var result = {};
  // 一个递归方法,合并值到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;
    }
  }

  // 每个参数都需要执行一遍,全都合并到result中去
  for (var i = 0, l = arguments.length; i < l; i++) {
    forEach(arguments[i], assignValue);
  }
  return result;
}

extend

extend 其实就是把 b 对象上的属性挨个覆盖在了 a 对象上。但是属性如果为函数,则函数的 this 指向却指向 thisArg,实现的很巧妙:

/**
 * 使用对象 b 的属性来扩展对象 a(修改了对象a本身)。
 *
 * @param {Object} a 要被扩展的对象
 * @param {Object} b 扩展属性的来源对象
 * @param {Object} thisArg 要绑定的this指向
 * @return {Object} 修改后的a对象
 */
function extend(a, b, thisArg) {
  forEach(b, function assignValue(val, key) {
    // 如果指定了this指向,并且此属性为函数,则重新绑定this指向
    if (thisArg && typeof val === 'function') {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  });
  return a;
}

stripBOM

这里的这个 BOM 不是 Browser Object Model(文档对象模型),而是 Byte Order Mark(字节顺序标记),Unicode标准 允许 UTF8 中有 BOM ,但是 UTF8 中已经不必需并且不建议使用,对 UTF8 已经毫无意义,所以删去。

/**
 * 删除Byte Order Mark。捕获 EF BB BF(UTF-8 BOM)
 * 主要是处理编码问题
 *
 * @param {string} content 带有 BOM 的内容
 * @return {string} 删除 BOM 后的内容
 */
function stripBOM(content) {
  if (content.charCodeAt(0) === 0xfeff) {
    content = content.slice(1);
  }
  return content;
}

/helpers

上篇已经分析过,/helps 目录中包含如下这些文件:

└─helpers
        bind.js
        buildURL.js
        combineURLs.js
        cookies.js
        deprecatedMethod.js
        isAbsoluteURL.js
        isAxiosError.js
        isURLSameOrigin.js
        normalizeHeaderName.js
        parseHeaders.js
        README.md
        spread.js
        validator.js

README.md 中,介绍了该目录的作用:helpers/ 中的模块是通用模块,特定于 axios 的内部专门情况。这些模块理论上可以发布到 npm 并由其他模块或应用程序使用。通用模块的一些示例如下:

  • 浏览器 polyfills
  • cookie 管理
  • 解析 HTTP 请求头

bind.js

bind 方法主要就是用闭包和 Function.prototype.apply 方法实现了 this 指向的转换,写法非常复古:

/**
 * @description: 修改fn的this指向为thisArg
 * @param {Function} fn
 * @param {Object} thisArg
 * @return {Function} 返回修改了this指向的fn
 */
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];
    }
    // wrap函数修改fn的指向为thisArg
    return fn.apply(thisArg, args);
  };
};

buildURL

这个方法就是 Axios 中如何处理 params 参数的那一部分,看完之后很真切的解答了我的诸多开发过程中的小疑惑,包括经过 axios 处理的 url,什么字符会被转义?为什么数组参数会在后面加一个“[]”?如果 url 中结尾已经有“?”,会不会变成两个问号?

var utils = require('./../utils');

function encode(val) {
  // 正常encodeURIComponent不转义的字符: A-Z a-z 0-9 - _ . ! ~ * ' ( )
  // 先转义val,再把:$,+[]这几个字符解码回来
  // 所以最后A-Z a-z 0-9 - _ . ! ~ * ' ( ) : $ , + [ ]这几个字符不转义,其他都转义
  return encodeURIComponent(val)
    .replace(/%3A/gi, ':')
    .replace(/%24/g, '$')
    .replace(/%2C/gi, ',')
    .replace(/%20/g, '+')
    .replace(/%5B/gi, '[')
    .replace(/%5D/gi, ']');
}

/**
 * 通过把params加到url的最后面来创建完整url
 *
 * @param {string} url  url的主机名 (例如 http://www.google.com)
 * @param {object} [params] 要添加的参数
 * @returns {string} 格式化后的参数 http://www.google.com?a=1&b=2
 */
module.exports = function buildURL(url, params, paramsSerializer) {
  // 没有参数就直接返回
  /*eslint no-param-reassign:0*/
  if (!params) {
    return url;
  }

  var serializedParams;
  // 如果有params序列方法,就执行下然后返回
  if (paramsSerializer) {
    serializedParams = paramsSerializer(params);
    // 如果是一个URLSearchParams对象,就返回toString()的结果
  } else if (utils.isURLSearchParams(params)) {
    serializedParams = params.toString();
    // 否则就进行普通序列化
  } else {
    var parts = [];

    utils.forEach(params, function serialize(val, key) {
      // 值为null或者undefined时,就不添加
      if (val === null || typeof val === 'undefined') {
        return;
      }

      // 值如果是个数组就给键封一层[]
      if (utils.isArray(val)) {
        key = key + '[]';
        // 值如果不是数组,就包装成数组
      } else {
        val = [val];
      }

      utils.forEach(val, function parseValue(v) {
        // 如果是date对象,就变为YYYY-MM-DDTHH:mm:ss.sssZ格式
        if (utils.isDate(v)) {
          v = v.toISOString();
          // 如果是个对象,就stringify
        } else if (utils.isObject(v)) {
          v = JSON.stringify(v);
        }
        // 最后把parts中的值变为key=value格式
        parts.push(encode(key) + '=' + encode(v));
      });
    });

    // 最后封装为key1=value1&key2=value2的格式
    serializedParams = parts.join('&');
  }

  if (serializedParams) {
    // 从url中提取出#之前的部分
    var hashmarkIndex = url.indexOf('#');
    if (hashmarkIndex !== -1) {
      url = url.slice(0, hashmarkIndex);
    }

    // 如果之前没有params就加个? 如果有params就加个&
    url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams;
  }

  return url;
};

cookies

这个方法利用 IFFE 实际返回了一个对象,对象中 writereadremove 方法,可以在浏览器环境中对 cookie 进行增删查改。这个方法对非浏览器环境做了兼容,在非浏览器环境中会执行一个空函数。

var utils = require('./../utils');

module.exports =
  // 先判断是不是标准的浏览器环境
  utils.isStandardBrowserEnv()
    ? // 标准的浏览器环境支持 document.cookie
      (function standardBrowserEnv() {
        return {
          // 把cookie的字段一个个写进去
          write: function write(name, value, expires, path, domain, secure) {
            var cookie = [];
            cookie.push(name + '=' + encodeURIComponent(value));

            if (utils.isNumber(expires)) {
              cookie.push('expires=' + new Date(expires).toGMTString());
            }

            if (utils.isString(path)) {
              cookie.push('path=' + path);
            }

            if (utils.isString(domain)) {
              cookie.push('domain=' + domain);
            }

            if (secure === true) {
              cookie.push('secure');
            }

            document.cookie = cookie.join('; ');
          },

          // 通过正则来读取cookie的值
          read: function read(name) {
            var match = document.cookie.match(
              new RegExp('(^|;\\s*)(' + name + ')=([^;]*)')
            );
            return match ? decodeURIComponent(match[3]) : null;
          },

          // 通过设置过期时间来移除cookie
          remove: function remove(name) {
            this.write(name, '', Date.now() - 86400000);
          },
        };
      })()
    : // 非标准的浏览器环境(web workers, react-native)不支持 document.cookie
      (function nonStandardBrowserEnv() {
        return {
          write: function write() {},
          read: function read() {
            return null;
          },
          remove: function remove() {},
        };
      })();

有了这个方法,下次写 cookies 时可以直接从 axios 中引入,不必再专门引入复杂的 cookies 管理库了:

import cookies from 'axios/lib/helpers/cookies.js';

cookies.write(name, value, expires, path, domain, secure);
cookies.remove(name);
cookies.read(name);

deprecatedMethod

这段代码很简单,就是有一些 api 可能会被废弃掉,给用户警告下。

/**
 * 警告开发人员:正在使用的方法已被弃用。
 *
 * @param {string} method 被遗弃的方法
 * @param {string} [instead] 替换的新方法
 * @param {string} [docs] 更多细节的文档地址
 */
module.exports = function deprecatedMethod(method, instead, docs) {
  try {
    console.warn(
      'DEPRECATED method `' +
        method +
        '`.' +
        (instead ? ' Use `' + instead + '` instead.' : '') +
        ' This method will be removed in a future release.'
    );

    if (docs) {
      console.warn('For more information about usage see ' + docs);
    }
  } catch (e) {
    /* Ignore */
  }
};

isAbsoluteURL

判断 url 是否为绝对路径,判断的依据来源于RFC 3986 标准,根据是否以 <scheme>://// 开头来判断。

/**
 * 判断给定的地址是否为绝对url
 *
 * @param {string} url 要检查的url
 * @returns {boolean} 如果为绝对url返回true,否则返回false
 */
module.exports = function isAbsoluteURL(url) {
  // 如果 URL 以“<scheme>://”或“//”开头,则该 URL 被视为绝对url。
  // RFC 3986 将 scheme 名称定义为以字母开头且后跟字母、数字、加号、句点或连字符的任意组合的字符序列。
  return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
};

isAxiosError

判断一个 Error 对象是否为 Axios 抛出,所有由 Axios 抛出的错误都有一个 isAxiosError 标识。

/**
 * 判断error是否为Axios抛出的
 *
 * @param {*} payload 要检测的error
 * @returns {boolean} 如果error为Axios抛出的则返回true,否则返回false
 */
module.exports = function isAxiosError(payload) {
  // 通过isAxiosError来判断
  return typeof payload === 'object' && payload.isAxiosError === true;
};

isURLSameOrigin

判断 location 和给定的 url 是否同源。在这里借助了浏览器的 a 标签进行 url 解析,然后判断协议、主机和端口是否相同。

var utils = require('./../utils');

module.exports = utils.isStandardBrowserEnv()
  ? // 标准浏览器完全支持给定URL与当前URL是否同源的检测
    (function standardBrowserEnv() {
      // 判断是否为IE浏览器
      var msie = /(msie|trident)/i.test(navigator.userAgent);
      var urlParsingNode = document.createElement('a');
      var originURL;

      /**
       * 解析一个URL,将其分解为各个部分
       *
       * @param {String} url 要解析的URL
       * @returns {Object} 返回各个部分组成的对象
       */
      function resolveURL(url) {
        var href = url;

        if (msie) {
          // IE浏览器需要设置两次才能标准化属性
          urlParsingNode.setAttribute('href', href);
          href = urlParsingNode.href;
        }

        urlParsingNode.setAttribute('href', href);

        // urlParsingNode 提供了 UrlUtils 接口 - http://url.spec.whatwg.org/#urlutils
        return {
          href: urlParsingNode.href,
          protocol: urlParsingNode.protocol
            ? urlParsingNode.protocol.replace(/:$/, '')
            : '',
          host: urlParsingNode.host,
          search: urlParsingNode.search
            ? urlParsingNode.search.replace(/^\?/, '')
            : '',
          hash: urlParsingNode.hash
            ? urlParsingNode.hash.replace(/^#/, '')
            : '',
          hostname: urlParsingNode.hostname,
          port: urlParsingNode.port,
          pathname:
            urlParsingNode.pathname.charAt(0) === '/'
              ? urlParsingNode.pathname
              : '/' + urlParsingNode.pathname,
        };
      }

      // 最后获得的location的各个部分组成的对象
      originURL = resolveURL(window.location.href);

      /**
       * 判断URL与location是否同源
       *
       * @param {String} requestURL 要检查的url
       * @returns {boolean} 如果同源则返回true,否则返回false
       */
      return function isURLSameOrigin(requestURL) {
        var parsed = utils.isString(requestURL)
          ? resolveURL(requestURL)
          : requestURL;
        // protocol、hostname、port都相等才是同源
        return (
          parsed.protocol === originURL.protocol &&
          parsed.host === originURL.host
        );
      };
    })()
  : // 非标准的浏览器环境(web workers, react-native)都默认为同源
    (function nonStandardBrowserEnv() {
      return function isURLSameOrigin() {
        return true;
      };
    })();

normalizeHeaderName

该方法给定了一个标准的 key,并且检查 headers 中对应的 key,如果不标准就替换。

var utils = require('../utils');

/**
 * @description: 标准化报文头部信息
 * @param {Object} headers 请求头对象
 * @param {String} normalizedName 标准化的请求头key
 */
module.exports = function normalizeHeaderName(headers, normalizedName) {
  utils.forEach(headers, function processHeader(value, name) {
    // 当请求头的key大小写不标准时,修改为标准的
    if (
      name !== normalizedName &&
      name.toUpperCase() === normalizedName.toUpperCase()
    ) {
      headers[normalizedName] = value;
      delete headers[name];
    }
  });
};

parseHeaders

该方法通过换行符 \nheaders 进行分解,转化为一个对象。

注意:有些 key 重复了就忽略掉,有些 key 重复了就追加

var utils = require('./../utils');

// 下面数组中的header,如果出现了重复,就忽略掉
// c.f. https://nodejs.org/api/http.html#http_message_headers
var ignoreDuplicateOf = [
  'age',
  'authorization',
  'content-length',
  'content-type',
  'etag',
  'expires',
  'from',
  'host',
  'if-modified-since',
  'if-unmodified-since',
  'last-modified',
  'location',
  'max-forwards',
  'proxy-authorization',
  'referer',
  'retry-after',
  'user-agent',
];

/**
 * 把报文头字符串解析为对象
 *
 * ```
 * Date: Wed, 27 Aug 2014 08:58:49 GMT
 * Content-Type: application/json
 * Connection: keep-alive
 * Transfer-Encoding: chunked
 * ```
 *
 * @param {String} headers 需要解析的报文头字符串
 * @returns {Object} 解析后的对象
 */
module.exports = function parseHeaders(headers) {
  var parsed = {};
  var key;
  var val;
  var i;

  // 没有header就返回空对象
  if (!headers) {
    return parsed;
  }

  // headers字符串首先通过换行符来分割为数组
  utils.forEach(headers.split('\n'), function parser(line) {
    // 拿到每个header的键和值
    i = line.indexOf(':');
    key = utils.trim(line.substr(0, i)).toLowerCase();
    val = utils.trim(line.substr(i + 1));

    if (key) {
      // 如果key在“重复则忽略”的名单中,并且重复了,就忽略掉
      if (parsed[key] && ignoreDuplicateOf.indexOf(key) >= 0) {
        return;
      }
      // 对“set-cookie”进行专门处理
      if (key === 'set-cookie') {
        parsed[key] = (parsed[key] ? parsed[key] : []).concat([val]);
      } else {
        // 普通的header,在值字符串后面追加重复的值
        parsed[key] = parsed[key] ? parsed[key] + ', ' + val : val;
      }
    }
  });

  return parsed;
};

spread

将数组形式的参数按次序给回调函数传参并且调用。

/**
 *
 * 用于调用函数和扩展参数数组的语法糖。
 *
 * 常见的用法是`Function.prototype.apply`。
 *
 *  ```js
 *  function f(x, y, z) {}
 *  var args = [1, 2, 3];
 *  f.apply(null, args);
 *  ```
 *
 * 使用 `spread` 来重写上例
 *
 *  ```js
 *  spread(function(x, y, z) {})([1, 2, 3]);
 *  ```
 *
 * @param {Function} callback
 * @returns {Function}
 */
module.exports = function spread(callback) {
  return function wrap(arr) {
    return callback.apply(null, arr);
  };
};

validator

这个文件主要是返回了三个方法:

  • isOlderVersion:判断 Axios 的版本相对大小
  • assertOptions:断言对象上的各个属性类型
  • validators:各种 JS 类型的校验器
// 引入package.json(package.json其实也就是个普通的json)
var pkg = require('./../../package.json');

// validators是个类型校验器组成的对象
var validators = {};

// eslint-disable-next-line func-names
['object', 'boolean', 'number', 'function', 'string', 'symbol'].forEach(
  function (type, i) {
    validators[type] = function validator(thing) {
      return typeof thing === type || 'a' + (i < 1 ? 'n ' : ' ') + type;
    };
  }
);

var deprecatedWarnings = {};
// 当前版本组成的数组
var currentVerArr = pkg.version.split('.');

/**
 * 比较 package.json 的版本
 * @param {string} version
 * @param {string?} thanVersion 被比较的版本
 * @returns {boolean}
 */
function isOlderVersion(version, thanVersion) {
  var pkgVersionArr = thanVersion ? thanVersion.split('.') : currentVerArr;
  var destVer = version.split('.');
  // 通过比较每个位置的版本来确定是否version为thanVersion的老版本
  for (var i = 0; i < 3; i++) {
    if (pkgVersionArr[i] > destVer[i]) {
      return true;
    } else if (pkgVersionArr[i] < destVer[i]) {
      return false;
    }
  }
  return false;
}

/**
 * 过渡的选项的校验器,整个方法是为了提醒用户老版本的某些选项被遗弃
 * @param {function|boolean?} validator
 * @param {string?} version
 * @param {string} message
 * @returns {function}
 */
validators.transitional = function transitional(validator, version, message) {
  var isDeprecated = version && isOlderVersion(version);

  // 格式化信息,
  function formatMessage(opt, desc) {
    return (
      '[Axios v' +
      pkg.version +
      "] Transitional option '" +
      opt +
      "'" +
      desc +
      (message ? '. ' + message : '')
    );
  }

  // eslint-disable-next-line func-names
  return function (value, opt, opts) {
    if (validator === false) {
      throw new Error(formatMessage(opt, ' has been removed in ' + version));
    }

    if (isDeprecated && !deprecatedWarnings[opt]) {
      deprecatedWarnings[opt] = true;
      // eslint-disable-next-line no-console
      console.warn(
        formatMessage(
          opt,
          ' has been deprecated since v' +
            version +
            ' and will be removed in the near future'
        )
      );
    }

    return validator ? validator(value, opt, opts) : true;
  };
};

/**
 * 断言对象的属性类型
 * @param {object} options
 * @param {object} schema
 * @param {boolean?} allowUnknown
 */

function assertOptions(options, schema, allowUnknown) {
  // options不是一个对象时,直接报错
  if (typeof options !== 'object') {
    throw new TypeError('options must be an object');
  }
  // 挨个校验选项的类型是否符合要求
  var keys = Object.keys(options);
  var i = keys.length;
  while (i-- > 0) {
    var opt = keys[i];
    var validator = schema[opt];
    if (validator) {
      var value = options[opt];
      var result = value === undefined || validator(value, opt, options);
      if (result !== true) {
        // 类型不符合要求时直接报错
        throw new TypeError('option ' + opt + ' must be ' + result);
      }
      continue;
    }
    // 不允许未知选项时,直接报错
    if (allowUnknown !== true) {
      throw Error('Unknown option ' + opt);
    }
  }
}

module.exports = {
  isOlderVersion: isOlderVersion,
  assertOptions: assertOptions,
  validators: validators,
};

可以发现,虽然官方说 /helpers 中的方法与 Axios 本身不耦合,但是像 validator.jsisAxiosError.js 仍然是强耦合的。

总结

Axios 项目的 dependencies 中没有任何依赖,各种基础工具方法都是自己手动实现的。很明显,Axios 长久以来稳定性离不开这些工具方法的支持,所以这些基础的工具方法实现是很值得学习的。

下一篇 Axios 源码解析(四):核心工具方法(1)来解析 /core 目录下的文件。

棒槌banner.png