本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
学习目标
- 如何调试axios源码;
- 如何学习优秀开源项目的代码,应用到自己的项目;
- axios源码中实用的工具函数;
源码地址
调试技巧
- 所有github项目在后面加
1s
即可线上打开vscode预览,类似github1s.com/axios/axios… - 本地打开项目并调试
git clone https://github.com/axios/axios.git
cd axios
npm start
打开 http://localhost:3000/
打开浏览器的开发中工具->source->axios->axios.js即可打断点调试
源码解读
isBuffer
是否为Buffer
由于axios可用于浏览器环境和node环境,所以会涉及node相关模块的使用
JavaScript 语言自身只有字符串数据类型,没有二进制数据类型。
但在处理像TCP流或文件流时,必须使用到二进制数据。因此在 Node.js中,定义了一个 Buffer 类,该类用来创建一个专门存放二进制数据的缓存区。
/**
* Determine if a value is a Buffer
* 先判断不是 `undefined`和`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使用
// 创建一个长度为 256、且用 0 填充的 Buffer。
const buf = Buffer.alloc(256);
// 写入字符串
len = buf.write("hello friends");
console.log("写入字节数 : "+ len); // 输出:写入字节数 : 13
// 创建一个包含 '0x1, 0x2, 0x3' 的 Buffer。
const ss = Buffer.from('1, 2, 3');
console.log(typeof ss.constructor) // 输出:function
console.log(ss.constructor.isBuffer(ss)) // 输出:true
// 读取 Node 缓冲区数据
console.log( ss.toString('utf8')); //输出: 1, 2, 3
const buf1 = Buffer.from([1, 2, 3]);
const json = JSON.stringify(buf1); // 这里会隐式地调用该 toJSON()转为## JSON 对象
console.log(json);
isStream
是否为Stream
Stream是Node中的一个抽象接口,很多对象都实现了此接口,比如http 服务器发起请求的request 对象、stdout(标准输出)等。
Stream 有四种流类型:
- Readable - 可读操作。
- Writable - 可写操作。
- Duplex - 可读可写操作.
- Transform - 操作被写入数据,然后读出结果。
所有的 Stream 对象都是 EventEmitter 的实例。常用的事件有:
- data - 当有数据可读时触发。
- end - 没有更多的数据可读时触发。
- error - 在接收和写入过程中发生错误时触发。
- finish - 所有数据已被写入到底层系统时触发。
/**
* Determine if a value is a Stream
* 判断val是对象且包含pipe方法
*/
function isStream(val) {
return isObject(val) && isFunction(val.pipe);
}
// Stream使用
var fs = require("fs");
var zlib = require('zlib');
var data = '';
// 创建可读流
var readerStream = fs.createReadStream('input.txt');
// 设置编码为 utf8。
readerStream.setEncoding('UTF8');
// 处理流事件 --> data, end, and error
readerStream.on('data', function(chunk) {
data += chunk;
});
readerStream.on('end',function(){
console.log(data);
});
// 压缩 input.txt 文件为 input.txt.gz
fs.createReadStream('input.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('input.txt.gz'));
isFormData
FormData
接口提供了一种表示表单数据的键值对 key/value
的构造方式,并且可以轻松的将数据通过XMLHttpRequest.send()
方法发送。
typeof FormData === 'function' node环境返回 false
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上
Object.prototype.toString
返回 [object FormData]
// Determine if a value is a FormData
function isFormData(thing) {
var pattern = '[object FormData]';
return thing && (
(typeof FormData === 'function' && thing instanceof FormData) ||
toString.call(thing) === pattern ||
(isFunction(thing.toString) && thing.toString() === pattern)
);
}
isFile
判断文件类型
function isFile(val) {
return Object.prototype.toString.call(val) === '[object File]';
}
var file = new File(["foo"], "foo.txt");
console.log(Object.prototype.toString.call(file)) // 输出: [object File]
isBlob
判断Blob
Blob
对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,通过使用 FileReader
的其它方法可以把 Blob 读取为字符串或者数据URL。
function isBlob(val) {
return Object.prototype.toString.call(val) === '[object Blob]';
}
// Blob使用
// 通过其它对象创建 Blob 对象
var debug = {hello: "world"};
var blob = new Blob([JSON.stringify(debug, null, 2)], {type : 'application/json'});
isURLSearchParams
判断URLSearchParams
URLSearchParams
接口定义了一些实用的方法来处理 URL 的查询字符串。
function isURLSearchParams(val) {
return Object.prototype.toString.call(val) === '[object URLSearchParams]';
}
// 例子
const url = new URL('https://juejin.cn/editor/drafts/7094229471175114789?q=URLUtils.searchParams&topic=api');
const searchParams = new URLSearchParams(url.search);
searchParams.has("topic") === true; // true
searchParams.get("topic") === "api"; // true
searchParams.getAll("topic"); // ["api"]
searchParams.append("topic", "webdev");
searchParams.toString(); // "q=URLUtils.searchParams&topic=api&topic=webdev"
searchParams.set("topic", "More webdev");
searchParams.toString(); // "q=URLUtils.searchParams&topic=More+webdev"
searchParams.delete("topic");
searchParams.toString(); // "q=URLUtils.searchParams"
trim
去除首尾空格
// `trim`方法不存在的话,用正则
function trim(str) {
return str.trim ? str.trim() : str.replace(/^\s+|\s+$/g, '');
}
isStandardBrowserEnv
判断标准浏览器环境
function isStandardBrowserEnv() {
if (typeof navigator !== 'undefined' && (navigator.product === 'ReactNative' ||
navigator.product === 'NativeScript' ||
navigator.product === 'NS')) {
return false;
}
return (
typeof window !== 'undefined' &&
typeof document !== 'undefined'
);
}
// 目前product属性返回浏览器的引擎(产品)名称,所有浏览器都返回“Gecko”
forEach 遍历对象或数组
function forEach(obj, fn) {
// Don't bother if no value provided
if (obj === null || typeof obj === 'undefined') {
return;
}
// 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 (var i = 0, l = obj.length; i < l; i++) {
fn.call(null, obj[i], i, obj);
}
} else {
// Iterate over object keys
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
fn.call(null, obj[key], key, obj);
}
}
}
}
- 数组的
forEach()
方法按升序为数组中含有效值的每一项执行一次callback
函数,那些已删除或者未初始化的项将被跳过。例如[1,2,,4] callback会跳过第三个仅执行三次。 备注: 除了抛出异常以外,没有办法中止或跳出forEach()
循环 for循环
使用更灵活,可使用break
随时退出循环for...of
语句在可迭代对象(包括Array
,Map
,Set
,String
,TypedArray
,arguments 对象等等)上创建一个迭代循环。数组建议使用for...in
语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。对象建议使用 通过obj.hasOwnProperty(prop)
判断自身属性
merge
递归合并多个对象
function merge(/* obj1, obj2, obj3, ... */) {
var result = {};
function assignValue(val, key) {
if (isPlainObject(result[key]) && isPlainObject(val)) { // 重复key则覆盖前面的值 值为对象则循环调用merge merge基础对象为前一次的值
result[key] = merge(result[key], val);
} else if (isPlainObject(val)) { // 新key的对象 循环调用merge merge基础对象为{}
result[key] = merge({}, val);
} else if (isArray(val)) { // 新key数组 则直接赋值 slice()方法返回一个新的数组对象
result[key] = val.slice();
} else { // 新key的其他类型值则直接赋值
result[key] = val;
}
}
// 循环入参 依次执行赋值函数
for (var i = 0, l = arguments.length; i < l; i++) {
forEach(arguments[i], assignValue);
}
return result;
}
extend
扩展对象属性
function extend(a, b, thisArg) {
forEach(b, function assignValue(val, key) {
if (thisArg && typeof val === 'function') {
a[key] = bind(val, thisArg);
} else {
a[key] = val;
}
});
return a;
}
// extend对相同的属性仅覆盖 不合并
extend({foo: {a: 1, c: 3}}, {foo: {a: 2, b: 3}}) // foo: {a: 2, b: 3}
merge({foo: {a: 1, c: 3}}, {foo: {a: 2, b: 3}}) // foo: {a: 2, c: 3, b: 3}
stripBOM
删除UTF-8
编码中BOM
function stripBOM(content) {
if (content.charCodeAt(0) === 0xFEFF) {
content = content.slice(1);
}
return content;
}
BOM
全称是Byte Order Mark
,它是一个Unicode
字符,通常出现在文本的开头,用来标识字节序。UTF-8
主要的优点是可以兼容ASCII
,前 128 个 UTF-8 字符与前 128 个 ASCII 字符(编号为 0-127) 精确匹配,这意味着现有的 ASCII 文本已经是有效的 UTF-8。但如果使用BOM
的话,这个好处就荡然无存了。
总结
浏览器环境开发是我们熟悉的区域,相对来说,node环境的开发薄弱一些,需要去做更多的学习和实践。