[源码阅读]axios:实用工具函数

137 阅读8分钟

前言

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

这是源码共读的第19期,链接阅读axios源码,发现了这些实用的基础工具函数 - 掘金 (juejin.cn)

源码

准备源码

我觉得参考文章没有将源码位置写的很详细,所以我单独将我们需要阅读的源码让放在了一个仓库里,这样clone和阅读都会比较方便

git clone git@github.com:summer-like-coding/studyAxiosSource.git

后面我们需要看的就是axiosutils工具库

代码

kindOf()

定义:用来判断类型

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));

首先使用解构赋值,直接将toString解构出来,这样后续在使用的时候,就会比较方便

kindOf()函数使用的是,自执行函数:意思就是自己声明完了,自己去调用一下,只能使用一次

意思就是

  • 传入object.create(null)
  • 传进去以后,执行了thing这个函数null
  • 这时候我们需要明确的是,cache其实即使我们传入的参数object.create(null)这个参数
  • 后续我们调用的时候传入的参数,其实会用在thing

其实我们代码可以变为

const kindOf = (function fn1(cache) {
  console.log("cache",cache);//{}
  return thing => {
    console.log("thing",thing);//[1,2,3]
    const str = toString.call(thing);
    return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());
  }
})(Object.create(null));
console.log("kindOf",kindOf)
let type = kindOf([1, 2, 3])
console.log("type",type);

但是你可能会疑惑,我明明写的是立即执行函数,为什么我还是需要再次调用呢?

我的理解是,因为我们确实立即执行了这个fn1这个函数,但是这个函数的返回值依然是一个函数

我们可以看一下输出

kindof thing => {
    console.log("thing",thing);
    const str = toString.call(thing);
    return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase());
  }

看出,返回的确实是个函数,这时候我们就需要再次调用这个返回的函数,并且为这个函数传入参数thing

thing (3) [1, 2, 3]
type array

其次关于这个Object.prototype.string.call()方法来判断数据类型,这个就不多介绍了,如果你们需要可以看我之前写的博客,里面有一些介绍,[源码阅读]Vue2源码系列(上) - 掘金 (juejin.cn)

kindOfTest()

定义:就是用来判断,你传入的类型和真正的类型是不是相同

const kindOfTest = (type) => {
  type = type.toLowerCase();
  return (thing) => kindOf(thing) === type
}
console.log("kindOfTest",kindOfTest("array")([1,2,3]));
console.log("kindOfTest", kindOfTest("object")([1, 2, 3]));
kindOfTest true
kindOfTest false

逻辑:

  • 当我们第一次调用函数时,我们传入array(我们认为的类型值)
  • 进行了toLowerCase()变为小写,并存起来
  • 后面我们返回一个函数,在函数里,我们进行了比较,判断类型是不是一致
  • 我觉得这里面使用了闭包(在函数里面返回函数),这样我们在返回的函数里就可以访问kindOfTest这个函数里面的值

typeOfTest()

定义:这个就是简单进行类型判断typeOf

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

注意:

typeOf可以判断出简单数据类型,以及function,但是他对引用类型和null判断不出来

isArray()

定义:判断这个是不是Array

其实我觉得他是将Array.isArray()这个方法从Array身上解构出来了,这样就可以单独判断

const {isArray} = Array;
console.log(isArray([1,2,3]));//true

isArrayinstance更加适合对array判断,它会将伪数组也认为是数组

Array.isArray() - JavaScript | MDN (mozilla.org)

isString()||isFunction()||isNumber()

定义:判断是不是String,在里面我们调用了typeOfTest(),因为typeOf可以判断出基本类型function

const isString = typeOfTest('string');
const isFunction = typeOfTest('function');
const isNumber = typeOfTest('number');

isPlainObject()

定义:判断这个是不是Object,纯对象,如果是纯对象,那么true,否则false

纯对象:用{}或者new Object创建的对象,或者Object.create()出来的,不包括数组等

const {getPrototypeOf} = Object;
const isPlainObject = (val) => {
  if (kindOf(val) !== 'object') {
    return false;
  }
  const prototype = getPrototypeOf(val);
  return prototype === null || prototype === Object.prototype;
}

逻辑:

  • 首先调用kindOf()这个函数,先去判断这个val的准确类型

  • 如果不是object,那么就绝对不是plainObject

  • 因为纯对象只能是{}或者new Object(),或者Object.create()出来的

  • 所以需要去看一下他的原型

    • prototype === null

      const obj = Object.create(null)
      Object.getPrototypeOf(obj)//null
      
    • prototype === Object.prototype

      {}new出来的进行包括

       Object.getPrototypeOf({})
       //{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
       Object.prototype
       //{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
      

isFormData()

定义:判断是不是FormData,因为可能我们axios,进行表单提交,所以需要对这个进行判断

const isFormData = (thing) => {
  const pattern = '[object FormData]';
  return thing && (
    (typeof FormData === 'function' && thing instanceof FormData) ||
    toString.call(thing) === pattern ||
    (isFunction(thing.toString) && thing.toString() === pattern)
  );
}

isURLSearchParams()

定义:判断这个是不是url

const isURLSearchParams = kindOfTest('URLSearchParams');
const paramsString = "q=URLUtils.searchParams&topic=api"
const searchParams = new URLSearchParams(paramsString);
console.log("isURLSerachParams",isURLSearchParams(searchParams));

首先我们先来看一下SearchParams

URLSearchParams

定义:一些用来处理URL的查询字符串

const paramsString = "q=URLUtils.searchParams&topic=api"
const searchParams = new URLSearchParams(paramsString);

这边我们构造了URLSearchParams(),这时候searchParams就是一个URLParams对象,这个对象是可以进行for...of的,说明他肯定是有Symbol.iterator的,他是具有迭代器的

console.log(searchParams);
URLSearchParams {}
[[Prototype]]: URLSearchParams
append: ƒ append()
delete: ƒ delete()
entries: ƒ entries()
forEach: ƒ forEach()
get: ƒ ()
getAll: ƒ getAll()
has: ƒ has()
keys: ƒ keys()
set: ƒ ()
sort: ƒ sort()
toString: ƒ toString()
values: ƒ values()
constructor: ƒ URLSearchParams()
Symbol(Symbol.iterator): ƒ entries()
Symbol(Symbol.toStringTag): "URLSearchParams"
[[Prototype]]: Object

可以清晰地看到具有Symbol.iterator这个属性

那么现在我们来看一下循环结果

for (let [key,value] of searchParams) {
  console.log("key"+key+":value"+value);
}
keyq:valueURLUtils.searchParams
keytopic:valueapi

可以看出,他是分为键值对的形式的

他还有一些其他的api,具体参考:URLSearchParams - Web API 接口参考 | MDN (mozilla.org)

其实他的处理方式就像一种特殊的对象,同样具有增删改查等功能

逻辑:

  • 首先我们调用kindOfTest(),并传入我们需要判断的类型URLSreachParams
  • 后续我们调用KindOf(),并且传入参数,判断所传参数是不是满足URLSerachParams

forEach()

定义:用于遍历对象或者数组

现在我们来回忆一下forEach是如何使用的

forEach接受一个参数,参数为一个函数,就是你希望以某种方式去遍历

const arr = [1,2,3]
arr.forEach((elem,index)=>console.log("elem:"+elem+";index:"+index))
"elem:a;index:0"
"elem:b;index:1"
"elem:c;index:2"

看一下源码是如何实现的:

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 (i = 0, l = obj.length; i < l; i++) {
      fn.call(null, obj[i], i, obj);
    }
  } else {
    // Iterate over object keys
    const keys = allOwnKeys ? Object.getOwnPropertyNames(obj) : Object.keys(obj);
    const len = keys.length;
    let key;

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

逻辑:

  • 先是判断未定义未赋值现象,这样的直接为空
  • 假如我们判断出来的不是一个object,那么我们直接将他放入一个数组里,这样就可以进行遍历了
  • forEach,对数组非常好用,因为数组是可迭代的
  • 所以我们先进行判断,是数组还是普通对象
  • 如果是数组的的话,那么我们可以直接调用函数(这里的函数意思就是:你想要以那种模式进行输出,或者一些其他的操作),传入了value,index,array
  • 如果不是数组,就是普通对象,那么我们就获取所有的key,用key取获取value,同样的传入那三个参数

测试:

const fn1 = (elem, index, obj) => {
  console.log(`obj:${obj}--index:${index}--elem:${elem}`);
}
forEach([1,2,3],fn1)
forEach({name:'summer',age:12},fn1)
obj[1,2,3]
obj:1,2,3--index:0--elem:1
obj:1,2,3--index:1--elem:2
obj:1,2,3--index:2--elem:3

obj {name: 'summer', age: 12}
obj:[object Object]--index:name--elem:summer
obj:[object Object]--index:age--elem:12

所以说,这样做的好处,我们也可以使对象使用forEach

因为普通的forEach不可以对普通对象使用

const obj = {name:'summer',age:14}
obj.forEach(element => console.log(element));//Error: obj.forEach is not a function

疑问:我们为什么要使用fn.call()呢?明明我们可以直接调用fn

merge()

定义:就是将两个对象进行合并

function merge(/* obj1, obj2, obj3, ... */) {
  const result = {};
  const 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 (let i = 0, l = arguments.length; i < l; i++) {
    arguments[i] && forEach(arguments[i], assignValue);
  }
  return result;
}

extend()

const extend = (a, b, thisArg, {allOwnKeys}= {}) => {
  forEach(b, (val, key) => {
    if (thisArg && isFunction(val)) {
      a[key] = bind(val, thisArg);
    } else {
      a[key] = val;
    }
  }, {allOwnKeys});
  return a;
}

作用:就是实现将b的属性添加到a上面

逻辑:

  • 因为需要实现的是:将b的属性添加到a
  • 所以很明显的,我们需要知道b上的属性,对b进行遍历
  • 如果这个val就是一个普通属性,那么我们在a上添加键值对
  • 但是:val:是一个函数的话,那么就需要让这个函数也是绑定在a上的,所以需要bind
  • 这里面的bind,第一个参数是函数名,而第二个参数是this指向

我们看一下axios处理的bind

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

搞了一个闭包,是内部的函数可以访问到外边的函数,这样使用apply,将这个函数绑定搭配thisArgs的身上

收获总结

axios这个工具库里面还是有很多值的我学习的地方

  • 它使用了很多的解构赋值,这样可以方便书写
  • 你可以深刻的体会到,什么叫做,一个函数只干一件事
  • 了解到了URLSearchParams这个对象,发现他也是能够增删改查的
  • 对于forEach,实现了普通对象也可以进行这种forEach遍历
  • 回顾了什么是立即执行函数,为啥需要立即执行
    • 我觉得,就是避免出现这种fn()()现象
  • 对于extend,这个是可以学习的,其实他就是进行遍历,然后添加上去,但是对函数,会进行一些处理,这边是用了bind
    • 但是这里的bind,和我们之前用的不太相同,axios对他进行了处理,至于为啥?你可以细细品一下🤷‍♀️
    • 我理解的,大概就是:正常的bindfun.bind(thisArgs,arg),你没法往bind前面添加函数名
    • 所以axiosbind重写了

参考文章:【axios源码】- 工具函数utils研读解析 - 掘金 (juejin.cn)