读了会 axios 源码,虽然云里雾里,但是我想到了三个有趣的对比

2,938 阅读6分钟

源码阅读

最近翻来了 axios 源码,信心满满的看了会,虽然哪跟哪都没串起来,但是意外收获了一些新的想法。

有几组不错的知识点,对比看,比单独看每个知识点,更有趣一些。

遇到有趣的知识点,当然要分享一下。

文章速读

本文从 axios 的源码联想到了几个不错的知识点对比。

阅读文章,可以有以下收获:

知识点对比开始

delete or undefined, 谁是更好的选择?

技术讨论小剧场

一:这里为什么要用 delete 删除这个属性。

某:因为异步请求里不能带上这个属性。

一:把值设置成 undefined,JSON.stringify 方法会过滤掉值为 undefined 的对象属性。

某:这个方法好像挺不错的。等等,我要是没有用到 JSON.stringify 方法呢?

一:写个判断方法。

某:键盘已递上。

undefined 值的判断方法

const typeOfTest = type => thing => typeof thing === type;

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

const val = undefined;
const val2 = 2;
isUndefined(val) // true
isUndefined(val2) // false

某:怎么写的这么快!

一:当然了,这可是 axios 源码里采用的方法。

某:所以结论就是,推荐将对象的值设置为 undefined,而不是delete 它。

一:是的。

某:再讲点 delete 的知识点。这个操作符,我还不太熟悉。

一:这个可以有。正好可以正向思维和逆向思维齐上阵。

delete 删除了什么?

删除对象的某个属性

我们会按照 MDN 文档定义的那样,「delete 操作符用于删除对象的某个属性」,先来尝试删除对象的属性:

var obj = {
  name: 'ye',
  age: 18,
};
delete obj.age;
console.log(delete obj.age); // true
console.log(obj); // { name: 'ye' }

非常符合预期的结果,delete 返回值是 true,且 obj 已不存在 age 属性。

如果删除一个对象不存在的属性,会怎样?

var obj = {
  name: 'ye',
  age: 18,
};
delete obj.desc;
console.log(delete obj.desc); // true
console.log(obj); // { name: 'ye', age: 18 }

这种情况也很好解释,MDN 官网已经给了文字解释:

如果你试图删除的属性不存在,那么 delete 将不会起任何作用,但仍会返回 true。

delete 'zhang' or delete 18 结果会怎样?

console.log(delete 'zhang'); // true
console.log(delete 18); // true

结果都是 true,有意思。其实,这里的字符串也好,数字也好,其实是单值表达式的结果,delete 删除的是这个表达式的结果,所以返回了 true。

所谓单值表达式是指没有运算符的表达式,一般值是其本身。

删除 let 或 const 声明的属性会怎样?

let name = 'ye';
or 
let name = obj.name;
console.log(delete name); // false

结果是 false,因为任何用 let 或 const 声明的属性不能够从它被声明的作用域中删除。

小结

  1. 不需要某个对象属性时,推荐将对象的值设置为 undefined,而不是delete 它。
  2. 经过一系列实验会发现,delete 其实删除的是一个表达式的引用类型的结果。
  3. axios 源码内容虽然不是特别多,但是完整的梳理清楚还是挺耗时的。我帮大家画个入口示意图,感兴趣的可以下载到本地,慢慢赏析。☞axios github

yield 和 return,这俩有故事?

技术讨论小剧场

一:为何眉头紧锁。

某:正在看 axios 的源码。

一:我理解了。

某:我不理解,这里为什么要用 yield。

一: 这么快看到这里了。

某:我只是先打开了这个文件而已。

品品为什么要用 yield

axios 源码里面有一段代码,在测试用例里面,实际的代码无法直接打印,我稍作了调整,如下:

const count = 10;
const chunkLength = 10;
const contentLength = count * chunkLength;
const samples = Array.from(
  (function* () {
    for (let i = 1; i <= 10; i++) {
      yield {
        loaded: chunkLength * i,
        total: contentLength,
        progress: (chunkLength * i) / contentLength,
        bytes: 4,
        download: true,
      };
    }
  })(),
);

打印 samples 的结果

某:如果这里的代码把 yield 改成 return 会怎样。

一:你猜。

不卖关子,其实会打印一个空数组。

console.log(samples); // []

某:能用 return 实现上面的截图中的数组吗?

一:当然可以。

某:键盘和膝盖双双奉上。

Array.from 支持通过伪数组对象创建数组对象。伪数组对象是指拥有一个 length 属性和若干索引属性的任意对象。

const count = 10;
const chunkLength = 10;
const contentLength = count * chunkLength;
const samples = Array.from({ length: count }, (_, j) => {
  let i = j + 1;
  return {
    loaded: chunkLength * i,
    total: contentLength,
    progress: (chunkLength * i) / contentLength,
    bytes: 4,
    download: true,
  };
});

yield 和 return 的故事

相同场景

这俩功能看着挺相似的,都能返回值给函数调用者。比如下面这两个代码块打印的结果是一样的。

使用 yield

function* foo(index) {
  while (index < 2) {
    index++;
    yield index;
  }
}

const iterator = foo(0);

console.log(iterator.next().value); // 1

使用 return

function* foo(index) {
  while (index < 2) {
    index++;
    return index;
  }
}

const iterator = foo(0);

console.log(iterator.next().value); // 1

如果把返回的这行代码提升会怎么样?

使用 yield

function* foo(index) {
  while (index < 2) {
    yield index;
    index++;
  }
}

const iterator = foo(0);

console.log(iterator.next().value); // 0
console.log(iterator.next().value); // 1

使用 return

function* foo(index) {
  while (index < 2) {
    return index;
    index++;
  }
}

const iterator = foo(0);

console.log(iterator.next().value); // 0
console.log(iterator.next().value); // undefined

结果不一样了。可见这俩有个明显的不同:

  • return 语句终止函数的执行。
  • yield 语句挂起当前函数,而下一次调用 next() 时,在 yield 之后紧接着的语句会继续执行

听说 yield 能传值?

来看下面这个例子

function* func(x) {
  console.log('x:', x);
  x--;
  x = yield x;
}
let generatorFunc = func(10);
let value = generatorFunc.next().value;
console.log('value:', value);

打印结果

x: 10 // 第一个console
value: 9 // 第二个console

解析一下,第一个 console,x 值来自函数的传参,这个很容易理解。第二个 console,这里 x 值来自调用 generatorFunc.next(arg) 时的传参 arg,而 arg 来自 yield 表达式的结果。

小结

  1. yield 关键字用于暂停和恢复生成器函数。
  2. yield 和 return 的故事在于,yield可以被认为是一个基于生成器的版本的 return 关键字。但是 yield 功能更丰富。
  3. axios 源码,看到哪里算哪里吧。

typeof and toString, 类型校验专场

技术讨论小剧场

某:还记得我们前面讨论的如何判断值为 undefined 吗?

一:当然,就在文章前面没多远的地方。

某:我又找到了一种判断方式。

一:替换方式?哪种更好?

某:我愿称之为 typeOfTest 的升级版。

toString:更辽阔的类型校验

先来看看 axios 源码是怎么实现对引用类型(其实包括基础类型)的校验。

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

const kindOfTest = type => {
  type = type.toLowerCase();
  return thing => kindOf(thing) === type;
};
/**
 * Determine if a value is a Date
 *
 * @param {*} val The value to test
 *
 * @returns {boolean} True if value is a Date, otherwise false
 */
const isDate = kindOfTest('Date');

console.log(isDate(new Date())); // true
console.log(isDate('ye')); // false

上面的方法,核心在于使用 Object.prototype.toString.call() 获取数据类型,它返回 "[object Type]",使用正则匹配可以得到最终的 Type。

下面我们用 Object.prototype.toString.call() 打印一下绝大多数的数据类型,看结果否和我们预期的一样:

console.log(Object.prototype.toString.call(111)); // [object Number]
console.log(Object.prototype.toString.call('ye')); // [object String]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object ReUndefinedgExp]
console.log(Object.prototype.toString.call(Symbol())); // [object Symbol]
console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(function () {})); // [object Function]
console.log(Object.prototype.toString.call(new Error())); // [object Error]
console.log(Object.prototype.toString.call(new Date())); // [object Date]
console.log(Object.prototype.toString.call(new RegExp())); // [object RegExp]
console.log(Object.prototype.toString.call(Map)); // [object Function]
console.log(Object.prototype.toString.call(Math)); // [object Math]

可以得到几乎所有的数据类型。

小结

  1. Object.prototype.toString.call() 几乎可以实现所有数据类型的校验。
  2. 想要获取准确的数据类型,还要在 [object Type] 的基础上通过正则匹配获取最终的 Type。
  3. 方法都是现成的,需要哪个用哪个。掌声送给 axios 源码。

总结

整个 axios 源码相较而言,不算特别多,但是想要全部梳理明白,还是需要一定的时间和精力的。不过,从一些工具函数文件出发,很容易吸收部分功能设计,比如 util 文件。

本文从 axios 源码中的功能结合日常开发,总结了三个有趣的对比实验,希望能对今后的开发有帮助。

另外,若川大佬的《学习 axios 源码整体架构,打造属于自己的请求库》,对 axios 源码的整体架构做了详细介绍,推荐阅读。

如果觉得文章还不错,就 点赞 + 收藏 一波,持续产出技术分享。

本文正在参加「金石计划 . 瓜分6万现金大奖」