- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 源码: github.com/axios/axios
1. 环境准备
git clone https://gihub.com/axios.axios
cd axios
npm start
打开 http://localhost:3000/
浏览器展现如下页面, 打开控制台 source, 展现 axios 源码
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'}
简单描述下上面的函数:
- 先声明函数 kindCache, 接收一个 cache 参数,
- 这个函数返回的是一个函数,接收一个任意变量 thing。
- 最后,得到 kindof 函数, 并传入初始实参 Object.create(null),就是内部返回的函数,缓存中存在,直接返回,不存在则存进缓存。
2.2 kindOfTest 判断传入的类型和真实的类型是否一致
const kindOfTest = (type) => {
type = type.toLowerCase()
return (thing) => kindof(thing) === type
}
kindOfTest('array')([123]) // true
执行逻辑:
- 当传入 array 第一次执行函数, array 是我们以为的类型
- 第二次传入 [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)
}
函数逻辑:
- 首先调用 kindof() 这个函数, 先判断这个 val的真实类型
- 如果不是 object,那么绝对不是 plainObject,因为 纯对象只能是 {} 或者 new Object(),或者 Object.create()出来的,所以需要看一下他的原型
- 判断目标对象的原型是不是 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]', 其他的如下图:
- 关于 FormData 更多的性质,详情见
MDN- FormData
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;
}
总结:
- typeof 能够判断出 除了 null 之外的基本数据类型和 function,typeof的返回值只能是 number、string、boolean、undefined、function、object
- instanceof 判断引用类型,判断基本类型会报错,原理是对判断的对象通过 proto 顺着原型链往上找,判断是否出现构造函数的 prototype 属性
- Object.prototype.toString.call() 两者都可判断,最准确
坚持下去,一个人的努力,一定会在某个时时刻等来想要的结果。如果没有,那就是不够努力。