【跟着若川读源码】axios源码中的这些实用的基础工具函数(下)

540 阅读9分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这是源码共读的第19期,链接:axios 源码中10多个工具函数

之前总觉得阅读源码是一件很了不起的事情,是只有大佬才会去做的事。但在工作了一段时间后,使用了很多的优秀的框架和库后,在感叹巧妙的设计之余——有时候也会想作者到底是怎么去实现的。于是就开始尝试阅读一些源码,发现其实源码也不是想象的那么难,至少有很多看得懂。

同时,源码有也有很多值得学习和借鉴的东西,不管是应用到日常开发中还是应对面试都是非常有用的,所以还是推荐大家也去尝试阅读的。  

前言

这是我的阅读源码的第二篇文章,逐渐培养每周阅读源码并写文章记录的习惯。

axios 是我们平时经常用到的网络请求库,可以同时用于 浏览器和node.js 环境中。今天我们来阅读 axios 源码,来了解其中的封装的工具函数。

如何阅读 github 上的源码

打开 axios 即可查看源码

在每一个github项目中的url里直接加上1s,就能在网页版vscode中查看源码了。(我在上面提供的链接已经添加了 1s

如果这个方法不可行的话,还可以使用下面的方法把源码克隆到本地中进行查看。

克隆 axios 项目查看源码

开源项目一般能在根目录下的README.md文件或CONTRIBUTING.md中找到贡献指南。贡献指南中说明了参与贡献代码的一些注意事项,比如:代码风格、代码提交注释格式、开发、调试等。

axios 中的工具函数

axios 的工具函数都在 utils.js 这个文件中,我们在平时也可以学习开源库的命名习惯,把工具函数放到 utils.js 文件中。

axios 源码的utils.js文件中有很多的工具函数,为了方便大家阅读,我将文章分为上、下两部分,本文是下半部分,介绍的是 axios 工具函数中的非常重要且可以用于日常开发中的函数。如:mergeextendforEach等。上半部分,介绍的是 axios 实现工具函数依赖的基本函数,还有工具函数中判断数据类型的函数

需要掌握的工具函数

trim 去除首尾空格

优先使用 string 的原型链上的 trim 方法,如果没有则使用自定义的方法

const trim = (str) => str.trim ?
  str.trim() : str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');

\s:空格
\uFEFF:字节次序标记字符(Byte Order Mark),也就是BOM,它是es5新增的空白符
\xA0:禁止自动换行空白符,相当于html中的 

forEach 遍历对象或数组

这里的 forEach 不同于原生提供的 forEach 方法,原生的 forEach 只能迭代数组类数组对象;而自定义 forEach 可以迭代数组对象

类数组对象:格式与数组结构类似,拥有length属性,可以通过索引来访问或设置里面的元素,但是不能使用数组的方法,就叫类数组对象。 如:函数体内的arguments对象。

// obj 要操作的对象 / 数组
// fn 要执行的回调函数
// allOwnKeys 为 true 的时候会对对象内部不可枚举的属性也提取出来,而为 false 的时候只会提取出对象内部可枚举的属性

function forEach(obj, fn, {allOwnKeys = false} = {}) {
  // Don't bother if no value provided
  // 如果值不存在,无需处理
  if (obj === null || typeof obj === 'undefined') {
    return;
  }

  let i;
  let l;

  // 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循环执行回调fn
    for (i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // Iterate over object keys
    // 是对象,for循环执行回调fn
    const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
    // Object.getOwnPropertyNames 可以获取对象中可枚举的属性和不可枚举的属性
    // Object.keys 只可以获取对象中可枚举的属性
    const len = keys.length;
    let key;

    for (i = 0; i < len; i++) {
      key = keys[i];
      fn.call(null, obj[key], key, obj);
    }
  }
}

这个函数中遍历数组时使用的是for循环,而遍历对象时使用的是for...in

  • for 循环无法用于遍历对象,性能也比直接使用原生的 forEach 要好
  • for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。

推荐大家去看一下这篇文章有了for循环 为什么还要forEach?

那么,为什么不统一使用for...in来处理呢?

我搜索了得出以下结果,for...in 就是为遍历对象属性而构建的,而 for 循环则是为遍历数组而构建的。请看MDN
image.png

merge 合并

merege 函数是用于合并对象的场景中,如果对象中有相同的 key ,则优先使用参数列表中位置靠后的对象的值。

// 这里的 isPlainObject 是上一篇文章的函数,isArray 是原生的 Array.isArray 函数
function merge(/* obj1, obj2, obj3, ... */) {
  const result = {};
  const assignValue = (val, key) => {
    if (isPlainObject(result[key]) && isPlainObject(val)) {
      // 如果result中的 key和当前的 key是相同的,则当前的val会覆盖掉result的 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 (let i = 0, l = arguments.length; i < l; i++) {
    // 这里使用的 forEach 是上面自定义的 forEach 方法  
    arguments[i] && forEach(arguments[i], assignValue);
  }
  return result;
}

下面我们来运行一下

// 例子 1
var result = merge({foo: 123}, {foo: 456});
console.log(result.foo); // outputs 456

// 例子2
var a = { foo: 123, bar: { abc: { def: 890 }, jkl: 678 } };
var b = { foo: 456 };
var result = merge(a, b);

console.log(result); // { foo: 456, bar: { abc: {def:890 }, jkl:678}}
result.bar.abc.def = 567;
console.error(result); // { foo: 456, bar: { abc: {def:567 }, jkl:678}}

extend用于扩展当前对象的属性

 // a 要扩展属性的对象
 // b 要被复制属性的对象
 // thisArg 绑定函数中的 this 指向
 // {allOwnKeys} 是 forEach 中的参数,请到 forEach 函数部分去查看
 
const extend = (a, b, thisArg, {allOwnKeys}= {}) => {
  forEach(b, (val, key) => {
    if (thisArg && isFunction(val)) {
      // isFunction 是上一篇文章中的方法
      // 如果是函数,则复制函数内容并绑定this
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  }, {allOwnKeys});
  return a;
}

这里使用的 bind 方法是内部自定义的 bind.js 文件,内容如下:

'use strict';

export default function bind(fn, thisArg) {
  return function wrap() {
    return fn.apply(thisArg, arguments);
  };
}

inherits是将原型方法从一个构造函数继承到另一个构造函数

 // constructor 子级构造函数
 // superConstructor 父级构造函数
 // props  需要添加到构造函数中的属性
 // descriptors 如果不为 undefined`,则传入对象自身定义的可枚举属性(不包括原型链上的枚举属性),可以不传这个参数。

const inherits = (constructor, superConstructor, props, descriptors) => {
  constructor.prototype = Object.create(superConstructor.prototype, descriptors);
  constructor.prototype.constructor = constructor;
  Object.defineProperty(constructor, 'super', {
    value: superConstructor.prototype
  });
  props && Object.assign(constructor.prototype, props);
}

这个函数的 descriptors 参数并没有在axios源码中用到,我全局搜索后没有找到例子。这个参数其实就是 Object.create 的第二个参数。

hasOwnProperty用于检查对象自身属性中是否具有指定的属性。

const hasOwnProperty = (
    ({hasOwnProperty}) => 
        (obj, prop) => hasOwnProperty.call(obj, prop)
    )(Object.prototype);

这个函数我刚开始没有看懂,但我们拆分来看就很简单了,可以写成下面的形式:

// 下面一行代码是 axios 中新定义的 hasOwnProperty 方法
const newHasOwnProperty = () => {
    // 下面一行代码是原生的  hasOwnProperty 方法
    const { hasOwnProperty } = Object.prototype;
    return (obj, prop) => hasOwnProperty.call(obj, prop)
}

endsWith 是用来确定字符串是否以指定字符串的字符结尾。

// str 目标字符串
// searchString 要查找的字符串
// position 开始查找的位置 

const endsWith = (str, searchString, position) => {
  // 把参数转换为字符串类型
  str = String(str);  // 这里的 String 等同于 new String
  if (position === undefined || position > str.length) {
    position = str.length;
  }
  position -= searchString.length;
  const lastIndex = str.indexOf(searchString, position);
  return lastIndex !== -1 && lastIndex === position;
}

toArray 用来将类数组对象转换为数组,如果失败则返回 null

// thing 类数组对象

const toArray = (thing) => {
  if (!thing) return null;
  if (isArray(thing)) return thing;
  let i = thing.length;
  if (!isNumber(i)) return null;
  const arr = new Array(i);
  while (i-- > 0) {
    arr[i] = thing[i];
  }
  return arr;
}

stripBOM 删除UTF-8编码中BOM

// content BOM字符串
 
function stripBOM(content) {
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
}

所谓 BOM,全称是Byte Order Mark,它是一个Unicode字符,通常出现在文本的开头,用来标识字节序。UTF-8主要的优点是可以兼容ASCII,但如果使用BOM的话,这个好处就荡然无存了。

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

 // regExp 要匹配的正则表达式
 // str 要查找的字符串
 
const matchAll = (regExp, str) => {
  let matches;
  const arr = [];

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

  return arr;
}

toCamelCase 转换为驼峰命名

const toCamelCase = str => {
  return str.toLowerCase().replace(/[_-\s]([a-z\d])(\w*)/g,
    function replacer(m, p1, p2) {
      return p1.toUpperCase() + p2;
    }
  );
};

noop 函数体为空的函数

const noop = () => {}

toFiniteNumber

toFiniteNumber 用于把无穷数转换成有穷数,如果是无穷数则设定为默认数,如果是有穷数则直接返回

const toFiniteNumber = (value, defaultValue) => {
  value = +value;
  return Number.isFinite(value) ? value : defaultValue;
}

进阶函数

这部分的函数有一定的难度,可以根据自己的情况酌情学习。

forEachEntry 迭代对象中的每一项,并调用函数(只适用于可迭代对象)

要搞清楚这个函数,需要先了解一下迭代器迭代协议

 // obj 要迭代的对象
 // fn 要执行的回调函数
 
const forEachEntry = (obj, fn) => {
  const generator = obj && obj[Symbol.iterator];

  const iterator = generator.call(obj);

  let result;

  while ((result = iterator.next()) && !result.done) {
    const pair = result.value;
    fn.call(obj, pair[0], pair[1]);
  }
}

可以看出这个函数和forEach的功能有点相似,那么,forEachEntryforEach 有什么区别呢?

  • forEachEntry 是对可迭代对象的每一项执行fn,只能作用于可迭代对象 ,forEach 可以处理任何数据类型
  • forEachEntry 可以通过定义迭代器的方式来控制循环的终止,forEach 对每一项都执行了回调

reduceDescriptors 确定值是否为正则对象

我特地去axios源码全局搜索了这个函数,没有发现在其他地方有使用到,只是用来实现下面的freezeMethods方法的辅助函数。

const isRegExp = kindOfTest('RegExp');

const reduceDescriptors = (obj, reducer) => {
  const descriptors = Object.getOwnPropertyDescriptors(obj);
  const reducedDescriptors = {};

  forEach(descriptors, (descriptor, name) => {
    if (reducer(descriptor, name, obj) !== false) {
      reducedDescriptors[name] = descriptor;
    }
  });

  Object.defineProperties(obj, reducedDescriptors);
}

freezeMethods 将函数设为不可修改(冻结函数)

const freezeMethods = (obj) => {
  reduceDescriptors(obj, (descriptor, name) => {
    const value = obj[name];

    if (!isFunction(value)) return;

    descriptor.enumerable = false;

    if ('writable' in descriptor) {
      descriptor.writable = false;
      return;
    }

    if (!descriptor.set) {
      descriptor.set = () => {
        throw Error('Can not read-only method \'' + name + '\'');
      };
    }
  });
}

toObjectSet 将数组或字符串转换为对象

const toObjectSet = (arrayOrString, delimiter) => {
  const obj = {};

  const define = (arr) => {
    arr.forEach(value => {
      obj[value] = true;
    });
  }

  isArray(arrayOrString) ? define(arrayOrString) : define(String(arrayOrString).split(delimiter));

  return obj;
}

toFlatObject将具有原型链的对象解析为平面对象(拍平对象)

 // sourceObj 源对象
 // destObj 目标对象
 // filter 过滤器
 // propFilter 传入参数的过滤器
 
const toFlatObject = (sourceObj, destObj, filter, propFilter) => {
  let props;
  let i;
  let prop;
  const 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 && getPrototypeOf(sourceObj);
  } while (sourceObj && (!filter || filter(sourceObj, destObj)) && sourceObj !== Object.prototype);

  return destObj;
}

遍历、迭代、可迭代对象

遍历:指的对数据结构的每一个成员进行有规律的且为一次访问的行为。
迭代:迭代是递归的一种特殊形式,是迭代器提供的一种方法,默认情况下是按照一定顺序逐个访问数据结构成员。迭代也是一种遍历行为。
可迭代对象:ES6中引入了 iterable 类型,Array Set Map String arguments NodeList 都属于 iterable,他们特点就是都拥有 [Symbol.iterator] 方法,包含他的对象被认为是可迭代的 iterable

总结

本文主要介绍了axios源码utils.js中的非常重要且可以用于日常开发中的函数,如:mergeextendforEach等。本文后半部分的进阶函数的难度就提升了不少,我也是花费了一些时间查阅了很多资料才理解了函数的含义。终于把这个坑填上了,学到了不少东西,也耗费了很多时间和精力,加油!!!