【源码共读】axios工具函数

190 阅读7分钟

1. 环境准备

git clone https://gihub.com/axios.axios
cd axios
npm start
打开 http://localhost:3000/

浏览器展现如下页面, 打开控制台 source, 展现 axios 源码 image.png

2. 工具函数

2.1 kindof 获取当前数据类型
var kindof = (cache => thing => {
  const str = toString.call(thing)
  return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase())
})(Object.create(null))

还原一下上面函数:

const {toString} = Object.prototype
function kindCache(cache){
    return function(thing){
    // 首先传入变量 thing, 通过 toString.call 获取当前变量的类型 [object XXXX]
    const str = toString.call(thing)
    // 然后从缓存 cache 里读取其类型, 有直接返回,没有则将类型放入 cache 中
    return cache[str] || (cache[str] = str.slice(8, -1).toLowerCase())
    }
}
// 最后得到返回数据的类型
const kindof = kindCache(Object.create(null))

// 初始化 cache  --> {}
kindof('Axios')  // string

// 此时的 cache --> {'[object String]': 'string'}
kindof(314)  // number

// 此时的 cache --> {'[object String]': 'string', '[object Number]': 'number'}
kindof(true)  // boolean

// 此时的 cache --> {'[object String]': 'string', '[object Number]': 'number', '[object Boolean]':'boolean'}

简单描述下上面的函数:

  1. 先声明函数 kindCache, 接收一个 cache 参数,
  2. 这个函数返回的是一个函数,接收一个任意变量 thing。
  3. 最后,得到 kindof 函数, 并传入初始实参 Object.create(null),就是内部返回的函数,缓存中存在,直接返回,不存在则存进缓存。
2.2 kindOfTest 判断传入的类型和真实的类型是否一致
const kindOfTest = (type) => {
    type = type.toLowerCase()
    return (thing) => kindof(thing) === type
}
kindOfTest('array')([123])  // true

执行逻辑:

  1. 当传入 array 第一次执行函数, array 是我们以为的类型
  2. 第二次传入 [123] , 作为 kindof 函数的参数, 获得到 [123]真实的数据类型, 与我们传入的数据类型 array 进行比较。 源码中用到 kindOfTest 声明的函数:
const isDate = kindOfTest('Date')
const isFile = kindOfTest('File')
const isBlob = kindOfTest('Blob')
const isFileList = kindOfTest('FileList')
const isURLSearchParams = kindOfTest('URLSearchParams');
2.3 typeOfTest 这也是判断数据类型是否和真实数据类型是否一致
const typeOfTest = type => thing => typeof thing === type;
typeOfTest('string')('test')   // true

上一个方法用的是 Object.prototype.toString 进行判断数据类型, 这里是用的 typeof 进行判断的 typeof 只能对简单数据类型和function进行判断,对引用类型和null 判断不出来。 源码中用到 typeOfTest声明的函数:

// undefined
const isUndefined = typeOfTest('undefind')
// string
const isString = typeOfTest('string')
// function
const isFunction = typeOfTest('function')
// number
const isNumber = typeOfTest('number')
2.4 isArray 判读是否是数组
function isArray(val){
    return toString.call(val) === "[object Array]"
}

// 环境允许也可以使用下面的方法
const {isArray} = Array.isArray
2.5 isBuffer 判断 buffer 类型
// 先判断是不是 undefine和null,再判断val存在构造函数,因为 Buffer 本身是一个类,最后通过自身的 isBuffer 方法判断
function isBuffer(val){
    return val !== null
    && !isUndefined(val)
    && val.constructor !== null
    && !isUndefined(val.constructor)
    && typeof val.constructor.isBuffer === 'function' &&
    val.constructor.isBuffer(val)
}

什么是 Buffer?

JS 本身只有字符串数据类型,没有二进制数据类型。 但在处理像 TCP 流或文件流时,必须使用到二进制数据。因此在 Node.js 中,定义了一个 Buffer 类,创建一个专门存放二进制数据的缓冲区。详细可以看 官方文档 或 更通俗易懂的解释。 因为 axios 可以运行在浏览器和 Node 环境中,所以内部会用到 node.js 相关的知识。

2.6 isArrayBuffer
function isArrayBuffer(val){
    return toString.call(val) === "[object ArrayBuffer]"
}
或者:
const isArrayBUffer = kindOfTest('isArrayBuffer')

2.7 isArrayBufferView
function isArrayBufferView(val){
let result;
if((typeof ArrayBuffer !== 'undefined') && (ArrayBuffer.isView)){
retult = ArrayBuffer.isView(val)
} else {
    result = (val) &&(val.buffer) && (isArrayBuffer(val.buffer))
}
return result;
}
  • 先判断是不是 undefined
  • 再判断 ArrayBuffer 原型链存在 isView 方法,如果原型链存在 isView 方法,则使用 ArrayBuffer.isView() 判断,否则调用上述封装 isArrayBuffer()方法
2.8 isObject 判断是否是 object 类型
function isObject(val){
    return val !== null && typeof(val) === 'object'
}
2.9 isBoolean 判断是否是Boolean类型
function isBoolean(val){
    return val === true || val === false
}
2.10 isPlainObject 判断是否是纯对象

纯对象: 用 {} 或者 new Object 创建的对象, 或者 Object.create()

  • Object.create(null) 创建的是一个空对象,在该对象上没有继承 Object.prototype 原型链上的属性和方法。

    Object.getPrototypeOf(null) === null

  • {}

    Object.getPrototypeOf({}) === Object.prototype

  • new Object()

    var obj1 = new Object()

    Object.getPrototypeOf(Object.getPrototypeOf(obj1)) === null

__proto__是一个内部属性,不是一个正式对外的API,在操作原型对象时应该
a.使用`Object.getPrototypeOf()`代替读取操作,  
b.使用`Object.setPrototypeOf()`代替设置操作

Object.getPrototypeOf(val) === val.__proto__ 
val.__proto__ === Object.prototype
const {getPrototypeOf} = Object
const isPlainObject = (val) => {
    if(kindOf(val) !== 'object'){
    return false
    }
    const property = getPrototypeOf(val)
    return (property === null || property === Object.prototype || Object.getPropertyOf(prototype) === null) && (Symbol.toStringTag in val) && !(Symbol.iterator in val)
}

函数逻辑:

  1. 首先调用 kindof() 这个函数, 先判断这个 val的真实类型
  2. 如果不是 object,那么绝对不是 plainObject,因为 纯对象只能是 {} 或者 new Object(),或者 Object.create()出来的,所以需要看一下他的原型
  3. 判断目标对象的原型是不是 null 或 Object.prototype
  • null
const obj = Object.create(null)
Object.getPrototypeOf(obj) // null
  • 对 {} 和 new 出来的进行判断
 Object.getPrototypeOf({})
 //{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
 Object.prototype
 //{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

2.11 isFormData() 判断是否是表单数据,因为axios进行表单提交,所以需要这个判断

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

Object.toString() 返回的是'[object FormData]', 其他的如下图: image.png

2.12 isStream 判断是否是流

function isStream(val){
    return isObject(val) && isFunction(val.pipe)
}

何为流? 流会将你想要从网络接收的资源分成一个个小的分块,然后按位处理它。这正是浏览器在接收用于显示web页面的资源时做的事情——视频缓冲区和更多的内容可以逐渐播放,有时候随着内容的加载,你可以看到图像逐渐地显示。

2.13 trim 去除首尾空格

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

2.14 forEach 遍历对象或者数组

forEach 遍历后的每一个元素作为参数传给 fn 函数调用

function forEach(obj, fn, {allOwnKeys = false} ={}){
    if(obj === null || typeof obj === 'undefined'){return}
    let i;
    let l;
    // 不是对象,强制转成数组类型
    if(typeof obj !== object){
        obj = [obj]
    }
    if(isArray(obj)){
        for(i=0;l=obj.length;i++){
        // 是数组,for 循环执行回调 fn
        fn.call(null, obj[i], i, obj))
        }
    } else {
        const keys = allOwnKeys ? Object.getOWnPropertyNames(obj) : Object.keys(obj)
        const len = keys.length;
        let key;
        // 是对象,for 循环执行 fn
        for(i=0;i<len;i++){
            key = keys[i]
            fn.call(null, obj[key], key, obj)
        }
    }
}
const f1 = (ele, index, obj) => {
  console.log(`obj:${ele}--index:${index}--elem:${obj}`)
}

forEach({name: 'hello', age: 18}, f1)

// obj:hello--index:name--elem:[object Object]
// obj:18--index:age--elem:[object Object]

函数逻辑:

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

2.15 findKey 查找对象中的 key 属性

function findKey(obj, key){
    key = key.toLowerCase()
    const keys = Object.keys(obj)
    let _key;
    let i = keys.length;
    while(i > 0){
        _key = keys[i]
        if(_key === key){
            return _key;
        }
    }
    return null;
}

2.16 merge 将两个对象进行合并

function merge(){
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)){
    // 是数组,slice 拷贝数据,返回一个副本
        result[key] = val.slice()
    } else {
        result[key] = val
    }
}
// 进行 for 循环, 其中 arguments 是一个对应于传递给函数的参数的类数组对象
// 每次遍历时调用 assignValue 方法, 入参为 forEach 工具函数返回的内容
for(let i = 0,l = arguments.length; i < l; i++){
// 配合 forEach 工具函数, 根据 arguments 长度 循环遍历其中的每一项, 后面的值会将前面的值覆盖。需要注意的是 arguments[i] 有可能是对象 Object 、数组 Array、null 或者未定义 undefined 
    arguments[i] && forEach(arguments[i], assignValue)
}
return result;
}

2.17 extend 将 b 属性 添加到 a 上

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; 
  }
  • 作用是将 Object b 属性和方法添加到 Object a 上
  • 如果待添加的 val 是 function 且运行存在明确的 this 指向 thisArg,需要通过调用 bind 绑定至当前对象即 a。第一个参数是函数名,第二个参数是 this 指向。
  • 如果待添加的 val 是一个普通属性, 直接在 a 上 添加相应的 key-val 键值对
// axios 处理 bind
function bind(fn, thisArg){
    return function wrap(){
        return fn.apply(thisArg, arguments)
    }
}
// 闭包 使内部函数可以访问到外边函数,这样使用 apply,将这个函数绑定搭配 thisArg 的身上

2.18 stripBOM 去掉字节顺序标记 BOM

function stripBom(content){
    if(content.charCodeAt(0) === 0xfeff){
        content = content.slice(1)
    }
    return content;
}
总结:
  1. typeof 能够判断出 除了 null 之外的基本数据类型和 function,typeof的返回值只能是 number、string、boolean、undefined、function、object
  2. instanceof 判断引用类型,判断基本类型会报错,原理是对判断的对象通过 proto 顺着原型链往上找,判断是否出现构造函数的 prototype 属性
  3. Object.prototype.toString.call() 两者都可判断,最准确

坚持下去,一个人的努力,一定会在某个时时刻等来想要的结果。如果没有,那就是不够努力。