我妈都看得懂的 Buffer 基础
在 JS 世界里,有一类看上去很高大上实质上很基础的东西:Buffer。今天好好介绍一下这厮。
什么是二进制对象
如果你曾经搜索过 buffer 相关的知识,你应该见过以下这几兄弟:ArrayBuffer,Uint8Array,Uint16Array,Uint32Array,TypedArray,Blob,DataView
... ... ...
然后没耐心的同学可能就关掉了搜索的一堆 tab 🙂
所以这几个东西到底是啥?在讲 buffer 之前,我们有必要搞清楚这几个东西的来龙去脉
ArrayBuffer 和它的兄弟们
ArrayBuffer 是最基础的二进制对象,是对固定长度的连续内存空间的引用
它虽然叫做 Array,但本质上和数组并没有什么关系。这玩意是申请了一段内存区域,但是里面放的是啥,我们并不知道,它只是存了一堆字节,也无法直接操作它。如果我们想操作这段内存空间怎么办呢?这时候就需要一个称之为视图的家伙
Uint8Array,Uint16Array,Uint32Array,这几个东西就是视图。你可以把它理解成 ArrayBuffer 的翻译器,只不过他们的翻译方式有点不同:
Uint8Array将ArrayBuffer中的每个字节视为一个单位。每个单位是 0 到 255 之间的数字。之所以是255,是因为每个单位最多是 8 位,即 2^8 次方。Uint16Array将ArrayBuffer中每 2 个字节视为一个单位。每个单位是 0 到 65535 之间的整数。原理同上。Uint32Array将ArrayBuffer中每 4 个字节视为一个单位。每个单位是 0 到 4294967295 之间的整数。原理同上。
// 我们可以通过 BYTES_PER_ELEMENT 静态属性来得之视图单位的大小
const buf8 = new Uint8Array();
const buf16 = new Uint16Array();
const buf32 = new Uint32Array();
console.log(buf8.BYTES_PER_ELEMENT); // 1
console.log(buf16.BYTES_PER_ELEMENT); // 2
console.log(buf32.BYTES_PER_ELEMENT); // 4
TypedArray
TypedArray 就是上面提到的 Uint8Array 那几个家伙的统称。他们都是 TypedArray (类型数组)的一种形式罢了。
需要注意的是,事实上并不存在
TypedArray这个构造函数。这只是一个统称。 除了上述的类型外,还有其他类型数组,比如Float64Array等。可以查一下 MDN,这里不多说了。
Blob
Blob 是浏览器环境上提供的一种承载原始数据的二进制对象,它和 ArrayBuffer 没有必然联系,但是又可以互相转化。可以简单理解成另一种形式的,没这么底层的 ArrayBuffer
DataView
DataView 是一种底层的,更灵活的读取 ArrayBuffer 的视图。 从上文中我们知道诸如 Unit8Array 之类的 “翻译器” (视图)可以用来翻译 ArrayBuffer,但有的时候我们并不想固化 “翻译” 类型,比如我有的时候想用 8 翻译,有的时候想用 16 翻译。DataView 就是一种更灵活的视图,他就能满足这个无理的需求。
const buffer = new ArrayBuffer(16); // 分配一个内存空间
const view = new DataView(buffer); // 创建 DataView 视图
view.setUint32(0, 4294967295); // 从第 0 个空间开始,以 32 位的形式写入数据
// 有时候我想以 8 位的形式 “翻译” 这个内存空间,从偏移量 0 开始翻译
console.log(view.getUint8(0)); // 255
// 今天心情好,想以 16 位的形式 “翻译” 这个内存空间,从偏移量 0 开始读
console.log(view.getUint16(0)); // 65535
// 今天心情超好,想以 32 位的形式 “翻译” 这个内存空间,从偏移量 0 开始读
console.log(view.getUint32(0)); // 4294967295
理清这几兄弟
为什么需要二进制对象呢,因为计算机就是用二进制来交流的。举个不恰当的例子,你总不能对一个法国人飙中文,不翻译一波他是听不懂的。计算机同理,不是二进制的话它也听不懂,一切语言最终也是要翻译一波。这也是为什么二进制效率高的原因,因为天生不需要翻译,机器听得懂。总结一下上面的几个概念:
ArrayBuffer是对内存的连续引用,这玩意没法直接操作。所以我们需要诸如Unit8Array等多种视图来 “翻译” 这厮- 我们将那堆视图称之为
TypedArray类型数组。但事实上并不存在TypedArray这个构造函数,它只是那堆视图的统称 Blob是另一种形式的二进制对象,主要在浏览器环境DataView是一种更灵活的视图,更灵活的操作ArrayBuffer
什么是 buffer
言归正传,前面铺垫了这么多,相信大家对二进制对象已经有所了解了。那么到底什么是 buffer 呢?
buffer 即缓存,是对二进制数据处理的一种方式。 有计算机基础的同学应该都知道,计算机只能处理二进制数据,没错就是你看电影时看的 010101 那堆看上去很高大上的东西。 无论是数字,字符串,图片,音视频,其实电脑最终存起来的都是一堆 0101。有的时候我们想处理或者搬运一些数据,就意味着我们要 “挪动” 这堆 0101。
一般来说我们一边挪,cpu 一边处理,这样并行就不会浪费时间。那么就涉及一个问题:如果挪的比 cpu 用的快,那么意味着会有一部分数据在等着被 cpu 用;如果挪的比 cpu 用的慢,那么 cpu 就要等待足够多的数据(构成有意义的能解读的整体)才能继续运算。
理想情况下,我们当然希望挪数据的时间和 cpu 处理的时间能一致,挪完一批 cpu 正好处理完上一批来接手新的一批。但事实上并不可能这么的巧。传输数据有可能比 cpu 快,也可能比 cpu 慢。无论哪种情况,那总归要一个位置去存储这些暂时用不上的数据(传的比 cpu 快,这个位置就存着提前到达的数据;比 cpu 慢,这个位置就存够一批足够能用的数据再给 cpu)这个区域就称之为缓存区(Buffer),一般是内存空间作为缓存空间。
可能有人会疑惑,为什么要从磁盘中读 IO 到内存缓存住,而不是 cpu 直接从磁盘中读数据呢?这是因为内存的读写速度比磁盘要快很多很多,归根结底缓存在更高速的内存中是为了让 cpu 用数据的时候更快,不用等待 IO。
Node 中的 Buffer
早年间的 js 并没有 Buffer,直到 ES6 推出才正式有了 ArrayBuffer 这个东西。但是 Node 是在服务端运行的,Buffer 对 Node 来说是刚需。早期的 Node 自己实现了一套非标准规范的 Buffer。通过 Buffer 的构造函数可以创建一个 Buffer 对象。而随着 Buffer 被纳入 js 标准规范,node 中的 Buffer 也逐渐适配新规范。现在的 Buffer 是在全局作用域中的,无需引入,开箱即用,且是 Uint8Array 类的子类。
node 中 Buffer 对象有很多 API,可以看官方文档了解。除了 Buffer 对象之外,Stream 和 Readline 也是比较常用的 node 内置二进制处理模块。简单介绍一下 Buffer 的基础 API
Buffer.alloc(size[, fill[, encoding]])创建一个 buffer 空间,可以填充制定元素,也可以指定编码类型Buffer.from()以 buffer 方式存储内容Buffer.from(array)创建一个 buffer 数组 buf,buf.values()返回一个可以迭代的对象Buffer.from(string[, encoding])创建一个 buffer 字符串,可以指定编码类型Buffer.from(buffer)拷贝一个 buffer 对象
有什么用
乍一眼看上去,Buffer 好像和我们常见的应用场景没什么关系。事实上这玩意体现在我们开发应用的方方面面。
前端人眼中的 Buffer
先从前端比较常见的说起。在 Web 开发中,我们可能需要对文件做一些处理。比如导出excel,下载文件,上传头像等等。 这些操作其实都是操作二进制数据。
// 伪代码示例
// Blob 上传图片
// fileHandler 为前置工作伪代码
// FileReader 加载图片,cavans 压缩图片等
const canvas = fileHandler()
// 创建 Blob 对象和 FormData
canvas.toBlob(blob => {
this.imgBlob = blob
}, 'image/jpeg')
let formdata = new FormData()
formdata.append('file', this.imgBlob, 'img.jpeg')
// 上传图片
axios({
headers: {
"Content-Type": "multipart/form-data"
},
method: "post",
url: uploadUrl,
data: formdata
})
.then(res => {
// do something
})
.catch(err => {
// do something
});
大前端眼中的 Buffer
大前端可能就涉及一些前端工程化,脚手架,打包工具等。这时候就可以通过流式处理操作 Buffer。比如按行读取某个配置文件,处理 webpack 的一些工作流,和 cli 交互读取和写入文件等等。
// 伪代码示例
// 逐行读取配置文件,个性化配置
const fs = require("fs");
const readline = require("readline");
// 创建可读流
const rl = readline.createInterface({
input: fs.createReadStream("theme.less"),
});
rl.on("line", (line: string) => {
if (line.trim().startsWith("configStart")) {
// 伪代码,处理变量
themeHandlerStart()
}
if (line.trim().startsWith("configStart")) {
// 伪代码,处理变量
themeHandlerEnd()
}
});
服务端眼中的 Buffer
对于服务端的同学来说,buffer 的应用就更广泛了 比如压缩和解压缩,比如加密解密,信息脱敏等等,其实都和 buffer 脱不了干系
此外因为 buffer 是操作二进制对象,所以他的性能和灵活性比常规的 js API 会强很多
- 比如要求更快速的响应时,http 直接传输 buffer 会比传输字符串的效率更高
- 比如日志持久化需要节省空间时,用不同的编码来压缩空间等等
用 Buffer 能更自由灵活的去调和时间复杂度和空间复杂度之间的关系
// 伪代码示例
// 压缩文件
const { createGzip } = require("zlib");
const { pipeline } = require("stream");
const { createReadStream, createWriteStream } = require("fs");
const gzip = createGzip();
const source = createReadStream("./package.json");
const destination = createWriteStream("./package.json.gz");
pipeline(source, gzip, destination, (err) => {
if (err) {
console.error("发生错误:", err);
process.exitCode = 1;
}
});
转载自:https://juejin.cn/post/6911487429471895560